diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/test/core/Texture.js b/test/core/Texture.js index 8c4ef98..995ed40 100644 --- a/test/core/Texture.js +++ b/test/core/Texture.js @@ -1,11 +1,26 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('PIXI.Texture', function () { it('should register Texture from Loader', function () { - const URL = 'foo.png'; - const NAME = 'bar'; + cleanCache(); + const image = new Image(); const texture = PIXI.Texture.fromLoader(image, URL, NAME); @@ -16,4 +31,100 @@ expect(PIXI.utils.TextureCache[URL]).to.equal(texture); expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); }); + + it('should remove Texture from cache on destroy', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + texture.destroy(); + expect(texture.textureCacheIds).to.equal(null); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly, ' + + 'and should remove only itself, not effecting the base texture and its cache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly using legacy addTextureToCache, ' + + 'and should remove also remove the base texture from its cache with removeTextureFromCache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addTextureToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeTextureFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should remove Texture from entire cache using removeFromCache (by Texture instance)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(texture); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove Texture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + }); }); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/test/core/Texture.js b/test/core/Texture.js index 8c4ef98..995ed40 100644 --- a/test/core/Texture.js +++ b/test/core/Texture.js @@ -1,11 +1,26 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('PIXI.Texture', function () { it('should register Texture from Loader', function () { - const URL = 'foo.png'; - const NAME = 'bar'; + cleanCache(); + const image = new Image(); const texture = PIXI.Texture.fromLoader(image, URL, NAME); @@ -16,4 +31,100 @@ expect(PIXI.utils.TextureCache[URL]).to.equal(texture); expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); }); + + it('should remove Texture from cache on destroy', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + texture.destroy(); + expect(texture.textureCacheIds).to.equal(null); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly, ' + + 'and should remove only itself, not effecting the base texture and its cache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly using legacy addTextureToCache, ' + + 'and should remove also remove the base texture from its cache with removeTextureFromCache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addTextureToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeTextureFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should remove Texture from entire cache using removeFromCache (by Texture instance)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(texture); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove Texture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + }); }); diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..188770f --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,322 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should destroy on listener', function (done) + { + const ticker = new Ticker(); + const listener2 = sinon.spy(); + const listener = sinon.spy(() => + { + ticker.destroy(); + setTimeout(() => + { + expect(listener2.called).to.be.false; + expect(listener.calledOnce).to.be.true; + done(); + }, 0); + }); + + ticker.add(listener); + ticker.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + ticker.start(); + }); +}); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/test/core/Texture.js b/test/core/Texture.js index 8c4ef98..995ed40 100644 --- a/test/core/Texture.js +++ b/test/core/Texture.js @@ -1,11 +1,26 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('PIXI.Texture', function () { it('should register Texture from Loader', function () { - const URL = 'foo.png'; - const NAME = 'bar'; + cleanCache(); + const image = new Image(); const texture = PIXI.Texture.fromLoader(image, URL, NAME); @@ -16,4 +31,100 @@ expect(PIXI.utils.TextureCache[URL]).to.equal(texture); expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); }); + + it('should remove Texture from cache on destroy', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + texture.destroy(); + expect(texture.textureCacheIds).to.equal(null); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly, ' + + 'and should remove only itself, not effecting the base texture and its cache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly using legacy addTextureToCache, ' + + 'and should remove also remove the base texture from its cache with removeTextureFromCache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addTextureToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeTextureFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should remove Texture from entire cache using removeFromCache (by Texture instance)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(texture); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove Texture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + }); }); diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..188770f --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,322 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should destroy on listener', function (done) + { + const ticker = new Ticker(); + const listener2 = sinon.spy(); + const listener = sinon.spy(() => + { + ticker.destroy(); + setTimeout(() => + { + expect(listener2.called).to.be.false; + expect(listener.calledOnce).to.be.true; + done(); + }, 0); + }); + + ticker.add(listener); + ticker.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + ticker.start(); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 8530131..28636ab 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -17,6 +17,7 @@ require('./util'); require('./Plane'); require('./Point'); +require('./Polygon'); require('./ObservablePoint'); require('./Matrix'); require('./Rectangle'); @@ -26,5 +27,7 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./BaseTexture'); require('./Texture'); +require('./Ticker'); require('./filters'); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/test/core/Texture.js b/test/core/Texture.js index 8c4ef98..995ed40 100644 --- a/test/core/Texture.js +++ b/test/core/Texture.js @@ -1,11 +1,26 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('PIXI.Texture', function () { it('should register Texture from Loader', function () { - const URL = 'foo.png'; - const NAME = 'bar'; + cleanCache(); + const image = new Image(); const texture = PIXI.Texture.fromLoader(image, URL, NAME); @@ -16,4 +31,100 @@ expect(PIXI.utils.TextureCache[URL]).to.equal(texture); expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); }); + + it('should remove Texture from cache on destroy', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + texture.destroy(); + expect(texture.textureCacheIds).to.equal(null); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly, ' + + 'and should remove only itself, not effecting the base texture and its cache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly using legacy addTextureToCache, ' + + 'and should remove also remove the base texture from its cache with removeTextureFromCache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addTextureToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeTextureFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should remove Texture from entire cache using removeFromCache (by Texture instance)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(texture); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove Texture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + }); }); diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..188770f --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,322 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should destroy on listener', function (done) + { + const ticker = new Ticker(); + const listener2 = sinon.spy(); + const listener = sinon.spy(() => + { + ticker.destroy(); + setTimeout(() => + { + expect(listener2.called).to.be.false; + expect(listener.calledOnce).to.be.true; + done(); + }, 0); + }); + + ticker.add(listener); + ticker.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + ticker.start(); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 8530131..28636ab 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -17,6 +17,7 @@ require('./util'); require('./Plane'); require('./Point'); +require('./Polygon'); require('./ObservablePoint'); require('./Matrix'); require('./Rectangle'); @@ -26,5 +27,7 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./BaseTexture'); require('./Texture'); +require('./Ticker'); require('./filters'); diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index c87fa5c..31385b9 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -1198,4 +1198,85 @@ expect(pointer.interaction.activeInteractionData[42]).to.be.undefined; }); }); + + describe('hitTest()', function () + { + it('should return hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10)); + + expect(hit).to.equal(graphics); + }); + + it('should return null if not hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(60, 60)); + + expect(hit).to.be.null; + }); + + it('should return top thing that was hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const behind = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(behind); + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + behind.beginFill(0xFFFFFF); + behind.drawRect(0, 0, 50, 50); + behind.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10)); + + expect(hit).to.equal(graphics); + }); + + it('should return hit when passing in root', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const behind = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(behind); + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + behind.beginFill(0xFFFFFF); + behind.drawRect(0, 0, 50, 50); + behind.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10), behind); + + expect(hit).to.equal(behind); + }); + }); }); diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/test/core/Texture.js b/test/core/Texture.js index 8c4ef98..995ed40 100644 --- a/test/core/Texture.js +++ b/test/core/Texture.js @@ -1,11 +1,26 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('PIXI.Texture', function () { it('should register Texture from Loader', function () { - const URL = 'foo.png'; - const NAME = 'bar'; + cleanCache(); + const image = new Image(); const texture = PIXI.Texture.fromLoader(image, URL, NAME); @@ -16,4 +31,100 @@ expect(PIXI.utils.TextureCache[URL]).to.equal(texture); expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); }); + + it('should remove Texture from cache on destroy', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + texture.destroy(); + expect(texture.textureCacheIds).to.equal(null); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly, ' + + 'and should remove only itself, not effecting the base texture and its cache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly using legacy addTextureToCache, ' + + 'and should remove also remove the base texture from its cache with removeTextureFromCache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addTextureToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeTextureFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should remove Texture from entire cache using removeFromCache (by Texture instance)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(texture); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove Texture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + }); }); diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..188770f --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,322 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should destroy on listener', function (done) + { + const ticker = new Ticker(); + const listener2 = sinon.spy(); + const listener = sinon.spy(() => + { + ticker.destroy(); + setTimeout(() => + { + expect(listener2.called).to.be.false; + expect(listener.calledOnce).to.be.true; + done(); + }, 0); + }); + + ticker.add(listener); + ticker.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + ticker.start(); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 8530131..28636ab 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -17,6 +17,7 @@ require('./util'); require('./Plane'); require('./Point'); +require('./Polygon'); require('./ObservablePoint'); require('./Matrix'); require('./Rectangle'); @@ -26,5 +27,7 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./BaseTexture'); require('./Texture'); +require('./Ticker'); require('./filters'); diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index c87fa5c..31385b9 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -1198,4 +1198,85 @@ expect(pointer.interaction.activeInteractionData[42]).to.be.undefined; }); }); + + describe('hitTest()', function () + { + it('should return hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10)); + + expect(hit).to.equal(graphics); + }); + + it('should return null if not hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(60, 60)); + + expect(hit).to.be.null; + }); + + it('should return top thing that was hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const behind = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(behind); + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + behind.beginFill(0xFFFFFF); + behind.drawRect(0, 0, 50, 50); + behind.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10)); + + expect(hit).to.equal(graphics); + }); + + it('should return hit when passing in root', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const behind = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(behind); + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + behind.beginFill(0xFFFFFF); + behind.drawRect(0, 0, 50, 50); + behind.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10), behind); + + expect(hit).to.equal(behind); + }); + }); }); diff --git a/test/loaders/spritesheetParser.js b/test/loaders/spritesheetParser.js index 79f8c15..4d54805 100644 --- a/test/loaders/spritesheetParser.js +++ b/test/loaders/spritesheetParser.js @@ -60,11 +60,46 @@ .that.is.an.instanceof(PIXI.Texture); }); + it('should build the image url', function () + { + function getResourcePath(url, image) + { + return PIXI.loaders.getResourcePath({ + url, + data: { meta: { image } }, + }); + } + + let result = getResourcePath('http://some.com/spritesheet.json', 'img.png'); + + expect(result).to.be.equals('http://some.com/img.png'); + + result = getResourcePath('http://some.com/some/dir/spritesheet.json', 'img.png'); + expect(result).to.be.equals('http://some.com/some/dir/img.png'); + + result = getResourcePath('http://some.com/some/dir/spritesheet.json', './img.png'); + expect(result).to.be.equals('http://some.com/some/dir/img.png'); + + result = getResourcePath('http://some.com/some/dir/spritesheet.json', '../img.png'); + expect(result).to.be.equals('http://some.com/some/img.png'); + + result = getResourcePath('/spritesheet.json', 'img.png'); + expect(result).to.be.equals('/img.png'); + + result = getResourcePath('/some/dir/spritesheet.json', 'img.png'); + expect(result).to.be.equals('/some/dir/img.png'); + + result = getResourcePath('/some/dir/spritesheet.json', './img.png'); + expect(result).to.be.equals('/some/dir/img.png'); + + result = getResourcePath('/some/dir/spritesheet.json', '../img.png'); + expect(result).to.be.equals('/some/img.png'); + }); + // TODO: Test that rectangles are created correctly. // TODO: Test that bathc processing works correctly. // TODO: Test that resolution processing works correctly. // TODO: Test that metadata is honored. - // TODO: Test data-url code paths. }); function createMockResource(type, data) diff --git a/.travis.yml b/.travis.yml index 3df8b22..14de334 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,3 +110,43 @@ on: all_branches: true condition: $TRAVIS_TAG + # Deploy config for latest release + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: dist + upload-dir: "release" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: docs + upload-dir: "release/docs" + on: + all_branches: true + condition: $TRAVIS_TAG + - provider: s3 + access_key_id: $S3_ACCESS_KEY_ID + secret_access_key: $S3_SECRET_ACCESS_KEY + bucket: "pixi.js" + skip_cleanup: true + acl: public_read + region: eu-west-1 + cache_control: "max-age=1209600" + local_dir: coverage + upload-dir: "release/coverage" + on: + all_branches: true + condition: $TRAVIS_TAG \ No newline at end of file diff --git a/README.md b/README.md index d26fadd..3b85f85 100644 --- a/README.md +++ b/README.md @@ -36,22 +36,14 @@ - Wiki: Other misc tutorials and resources are [on the Wiki](https://github.com/pixijs/pixi.js/wiki/Resources). ### Community ### -- Forums: Check out the [forum] (http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow] (http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. +- Forums: Check out the [forum](http://www.html5gamedevs.com/forum/15-pixijs/) and [Stackoverflow](http://stackoverflow.com/search?q=pixi.js), both friendly places to ask your pixi questions. - Inspiration: Check out the [gallery](http://www.pixijs.com/gallery) to see some of the amazing things people have created! - Chat: You can join us on [Gitter](https://gitter.im/pixijs/pixi.js) To chat about Pixi. We also now have a Slack channel. If you would like to join it please Send me an email (mat@goodboydigital.com) and I will invite you in. ### Setup ### -It's easy to get started with Pixi.js! Simply grab the pre-built versions from here: - -Release Branch - Nice and stable Pixi.js -- Unminified: [http://pixijs.download/release/pixi.js] -- Minified: [http://pixijs.download/release/pixi.min.js] - -Develop Branch - The bleeding edge version of Pixi.js -- Unminified: [http://pixijs.download/dev/pixi.js] -- Minified: [http://pixijs.download/dev/pixi.min.js] +It's easy to get started with Pixi.js! Simply download a [prebuilt build](https://github.com/pixijs/pixi.js/wiki/FAQs#where-can-i-get-a-build)! Alternatively, Pixi.js can be installed with [Bower](https://bower.io/#getting-started), [npm](https://docs.npmjs.com/getting-started/what-is-npm) or simply using a content delivery network (CDN) URL to embed Pixi.js directly on your HTML page. @@ -70,10 +62,10 @@ #### CDN Install (via cdnjs) ```html - + ``` -_Note: `4.2.2` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ +_Note: `4.4.3` can be replaced by any [released](https://github.com/pixijs/pixi.js/releases) version._ ### Demos ### @@ -130,7 +122,7 @@ // and the root stage PIXI.Container. var app = new PIXI.Application(); -// The application will create a canvas element for you that you +// The application will create a canvas element for you that you // can then insert into the DOM. document.body.appendChild(app.view); @@ -150,7 +142,7 @@ // Add the bunny to the scene we are building. app.stage.addChild(bunny); - + // Listen for frame updates app.ticker.add(function() { // each frame we spin the bunny around a bit diff --git a/bower.json b/bower.json deleted file mode 100644 index 41b668c..0000000 --- a/bower.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "pixi.js", - "main": "dist/pixi.min.js", - "ignore": [ - "**/.*", - "docs", - "node_modules", - "bower_components", - "test", - "scripts", - "bower.json", - "inch.json" - ], - "dependencies": { - }, - "devDependencies": { - } -} diff --git a/package.json b/package.json index d371ae8..ea9a0c4 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "lib": "babel src --out-dir lib -s", "predocs": "rimraf docs/**", "docs": "jsdoc -c scripts/jsdoc.conf.json -R README.md", - "publish:patch": "npm version patch --no-git-tag-version && npm publish", - "publish:minor": "npm version minor --no-git-tag-version && npm publish", - "publish:major": "npm version major --no-git-tag-version && npm publish", + "publish:patch": "npm version patch && npm publish", + "publish:minor": "npm version minor && npm publish", + "publish:major": "npm version major && npm publish", "postversion": "npm run clean && npm run build && npm run lib && npm test", - "postpublish": "node scripts/release.js" + "postpublish": "git push && git push --tags" }, "files": [ "dist/", @@ -73,7 +73,6 @@ "electron": "^1.4.15", "eslint": "^3.5.0", "floss": "^2.0.1", - "gh-pages": "^0.11.0", "jaguarjs-jsdoc": "^1.0.1", "js-md5": "^0.4.1", "jsdoc": "^3.4.2", diff --git a/scripts/jsdoc.conf.json b/scripts/jsdoc.conf.json index 553e61c..3f52180 100644 --- a/scripts/jsdoc.conf.json +++ b/scripts/jsdoc.conf.json @@ -43,7 +43,7 @@ }, "markdown" : { "parser" : "gfm", - "hardwrap" : true + "hardwrap" : false }, "opts": { "encoding" : "utf8", diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index 156620c..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// Publish script to push releases of the bin files -// the normally are gitignored -const ghpages = require('gh-pages'); -const path = require('path'); -const packageInfo = require(path.join(__dirname, '..', 'package.json')); - -const options = { - src: [ - 'dist/**/*', - 'lib/**/*', - 'src/**/*', - 'scripts/*', - 'scripts/renders/*', - 'test/**/*', - '*.json', - '*.md', - 'LICENSE', - '.eslintrc', - '.editorconfig', - '.travis.yml', - '.babelrc', - ], - dotfiles: true, - branch: 'release', - message: packageInfo.version, - logger: console.log.bind(console), -}; - -ghpages.publish(process.cwd(), options, (err) => -{ - if (err) - { - console.log(err); - process.exit(1); - - return; - } - - process.exit(0); -}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index f16bcb1..3c440df 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -21,12 +21,12 @@ const DIV_HOOK_ZINDEX = 2; /** - * The Accessibility manager reacreates the ability to tab and and have content read by screen + * The Accessibility manager recreates the ability to tab and have content read by screen * readers. This is very important as it can possibly help people with disabilities access pixi * content. * * Much like interaction any DisplayObject can be made accessible. This manager will map the - * events as if the mouse was being used, minimizing the efferot required to implement. + * events as if the mouse was being used, minimizing the effort required to implement. * * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility * @@ -80,7 +80,7 @@ this.renderId = 0; /** - * Setting this to true will visually show the divs + * Setting this to true will visually show the divs. * * @type {boolean} */ @@ -110,7 +110,7 @@ this._onMouseMove = this._onMouseMove.bind(this); /** - * stores the state of the manager. If there are no accessible objects or the mouse is moving the will be false. + * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false. * * @member {Array<*>} * @private @@ -151,7 +151,7 @@ /** * Activating will cause the Accessibility layer to be shown. This is called when a user - * preses the tab key + * preses the tab key. * * @private */ @@ -177,7 +177,7 @@ /** * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves - * the mouse + * the mouse. * * @private */ @@ -202,7 +202,7 @@ } /** - * This recursive function will run throught he scene graph and add any new accessible objects to the DOM layer. + * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer. * * @private * @param {PIXI.Container} displayObject - The DisplayObject to check. @@ -409,7 +409,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseover) + * Maps the div focus events to pixi's InteractionManager (mouseover) * * @private * @param {FocusEvent} e - The focus event. @@ -422,7 +422,7 @@ } /** - * Maps the div focus events to pixis InteractionManager (mouseout) + * Maps the div focus events to pixi's InteractionManager (mouseout) * * @private * @param {FocusEvent} e - The focusout event. diff --git a/src/accessibility/index.js b/src/accessibility/index.js index 568cfb1..0ae108f 100644 --- a/src/accessibility/index.js +++ b/src/accessibility/index.js @@ -1,4 +1,9 @@ /** + * This namespace contains a renderer plugin for interaction accessibility for end-users + * with physical impairments which require screen-renders, keyboard navigation, etc. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.accessibility */ export { default as accessibleTarget } from './accessibleTarget'; diff --git a/src/core/Application.js b/src/core/Application.js index 5340578..2fcc729 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -1,6 +1,8 @@ import { autoDetectRenderer } from './autoDetectRenderer'; import Container from './display/Container'; import { shared, Ticker } from './ticker'; +import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -22,28 +24,52 @@ */ export default class Application { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=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. + * @param {boolean} [options.sharedTicker=false] - `true` to use PIXI.ticker.shared, `false` to create new ticker. + * @param {boolean} [options.sharedLoader=false] - `true` to use PIXI.loaders.shared, `false` to create new Loader. */ - constructor(width, height, options, noWebGL, useSharedTicker = false) + constructor(options, arg2, arg3, arg4, arg5) { + // Support for constructor(width, height, options, noWebGL, useSharedTicker) + if (typeof options === 'number') + { + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + forceCanvas: !!arg4, + sharedTicker: !!arg5, + }, arg3); + } + + /** + * The default options, so we mixin functionality later. + * @member {object} + * @protected + */ + this._options = options = Object.assign({ + sharedTicker: false, + forceCanvas: false, + sharedLoader: false, + }, options); + /** * WebGL renderer if available, otherwise CanvasRenderer * @member {PIXI.WebGLRenderer|PIXI.CanvasRenderer} */ - this.renderer = autoDetectRenderer(width, height, options, noWebGL); + this.renderer = autoDetectRenderer(options); /** * The root display container that's rendered. @@ -63,7 +89,7 @@ * @member {PIXI.ticker.Ticker} * @default PIXI.ticker.shared */ - this.ticker = useSharedTicker ? shared : new Ticker(); + this.ticker = options.sharedTicker ? shared : new Ticker(); // Start the rendering this.start(); @@ -78,7 +104,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -136,13 +162,18 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; this.renderer.destroy(removeView); this.renderer = null; + + this._options = null; } } diff --git a/src/core/autoDetectRenderer.js b/src/core/autoDetectRenderer.js index 8960425..9ec0364 100644 --- a/src/core/autoDetectRenderer.js +++ b/src/core/autoDetectRenderer.js @@ -2,6 +2,7 @@ import CanvasRenderer from './renderers/canvas/CanvasRenderer'; import WebGLRenderer from './renderers/webgl/WebGLRenderer'; +// eslint-disable-next-line valid-jsdoc /** * This helper function will automatically detect which renderer you should be using. * WebGL is the preferred renderer as it is a lot faster. If webGL is not supported by @@ -9,24 +10,32 @@ * * @memberof PIXI * @function autoDetectRenderer - * @param {number} [width=800] - the width of the renderers view - * @param {number} [height=600] - the height of the renderers view * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the renderers view + * @param {number} [options.height=600] - the height of the renderers view * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.antialias=false] - sets antialias (only applicable in chrome at the moment) * @param {boolean} [options.preserveDrawingBuffer=false] - enables drawing buffer preservation, enable this if you * 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.forceCanvas=false] - prevents selection of WebGL renderer, even if such is present * @return {PIXI.WebGLRenderer|PIXI.CanvasRenderer} Returns WebGL renderer if available, otherwise CanvasRenderer */ -export function autoDetectRenderer(width = 800, height = 600, options, noWebGL) +export function autoDetectRenderer(options, arg1, arg2, arg3) { - if (!noWebGL && utils.isWebGLSupported()) + // Backward-compatible support for noWebGL option + let forceCanvas = options && options.forceCanvas; + + if (arg3 !== undefined) { - return new WebGLRenderer(width, height, options); + forceCanvas = arg3; } - return new CanvasRenderer(width, height, options); + if (!forceCanvas && utils.isWebGLSupported()) + { + return new WebGLRenderer(options, arg1, arg2); + } + + return new CanvasRenderer(options, arg1, arg2); } diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/display/DisplayObject.js b/src/core/display/DisplayObject.js index f5d615c..decb42a 100644 --- a/src/core/display/DisplayObject.js +++ b/src/core/display/DisplayObject.js @@ -13,7 +13,6 @@ * * @class * @extends EventEmitter - * @mixes PIXI.interaction.interactiveTarget * @memberof PIXI */ export default class DisplayObject extends EventEmitter @@ -122,6 +121,20 @@ * @readonly */ this._destroyed = false; + + /** + * Fired when this DisplayObject is added to a Container. + * + * @event PIXI.DisplayObject#added + * @param {PIXI.Container} container - The container added to. + */ + + /** + * Fired when this DisplayObject is removed from a Container. + * + * @event PIXI.DisplayObject#removed + * @param {PIXI.Container} container - The container removed from. + */ } /** diff --git a/src/core/graphics/Graphics.js b/src/core/graphics/Graphics.js index be4c4fb..9def67f 100644 --- a/src/core/graphics/Graphics.js +++ b/src/core/graphics/Graphics.js @@ -792,8 +792,8 @@ sprite.worldAlpha = this.worldAlpha * sprite.alpha; sprite.blendMode = this.blendMode; - sprite.texture._frame.width = rect.width; - sprite.texture._frame.height = rect.height; + sprite._texture._frame.width = rect.width; + sprite._texture._frame.height = rect.height; sprite.transform.worldTransform = this.transform.worldTransform; @@ -1071,7 +1071,7 @@ canvasRenderer.render(this, canvasBuffer, true, tempMatrix); - const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode); + const texture = Texture.fromCanvas(canvasBuffer.baseTexture._canvasRenderTarget.canvas, scaleMode, 'graphics'); texture.baseTexture.resolution = resolution; texture.baseTexture.update(); diff --git a/src/core/index.js b/src/core/index.js index f03fe15..3265dfb 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -26,6 +26,7 @@ export { default as SpriteRenderer } from './sprites/webgl/SpriteRenderer'; export { default as Text } from './text/Text'; export { default as TextStyle } from './text/TextStyle'; +export { default as TextMetrics } from './text/TextMetrics'; export { default as Graphics } from './graphics/Graphics'; export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; diff --git a/src/core/renderers/SystemRenderer.js b/src/core/renderers/SystemRenderer.js index a24c775..b9f192d 100644 --- a/src/core/renderers/SystemRenderer.js +++ b/src/core/renderers/SystemRenderer.js @@ -19,11 +19,12 @@ */ export default class SystemRenderer extends EventEmitter { + // eslint-disable-next-line valid-jsdoc /** * @param {string} system - The name of the system this renderer is for. - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -37,27 +38,31 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(system, screenWidth, screenHeight, options) + constructor(system, options, arg2, arg3) { super(); sayHello(system); - // prepare options - if (options) + // Support for constructor(system, screenWidth, screenHeight, options) + if (typeof options === 'number') { - for (const i in settings.RENDER_OPTIONS) - { - if (typeof options[i] === 'undefined') - { - options[i] = settings.RENDER_OPTIONS[i]; - } - } + options = Object.assign({ + width: options, + height: arg2 || settings.RENDER_OPTIONS.height, + }, arg3); } - else - { - options = settings.RENDER_OPTIONS; - } + + // Add the default render options + options = Object.assign({}, settings.RENDER_OPTIONS, options); + + /** + * The supplied constructor options. + * + * @member {Object} + * @readOnly + */ + this.options = options; /** * The type of the renderer. @@ -75,7 +80,7 @@ * * @member {PIXI.Rectangle} */ - this.screen = new Rectangle(0, 0, screenWidth || 800, screenHeight || 600); + this.screen = new Rectangle(0, 0, options.width, options.height); /** * The canvas element that everything is drawn to @@ -279,6 +284,8 @@ this.blendModes = null; + this.options = null; + this.preserveDrawingBuffer = false; this.clearBeforeRender = false; diff --git a/src/core/renderers/canvas/CanvasRenderer.js b/src/core/renderers/canvas/CanvasRenderer.js index e84e49c..691caf0 100644 --- a/src/core/renderers/canvas/CanvasRenderer.js +++ b/src/core/renderers/canvas/CanvasRenderer.js @@ -17,10 +17,11 @@ */ export default class CanvasRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -34,9 +35,9 @@ * @param {boolean} [options.roundPixels=false] - If true Pixi will Math.floor() x/y values when rendering, * stopping pixel interpolation. */ - constructor(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('Canvas', screenWidth, screenHeight, options); + super('Canvas', options, arg2, arg3); this.type = RENDERER_TYPE.CANVAS; @@ -96,7 +97,19 @@ this.context = null; this.renderingToScreen = false; - this.resize(screenWidth, screenHeight); + this.resize(this.options.width, this.options.height); + + /** + * Fired after rendering finishes. + * + * @event PIXI.CanvasRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.CanvasRenderer#prerender + */ } /** @@ -299,4 +312,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.CanvasRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.CanvasExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.CanvasPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.CanvasRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(CanvasRenderer); diff --git a/src/core/renderers/webgl/WebGLRenderer.js b/src/core/renderers/webgl/WebGLRenderer.js index 788ed6c..d9a1777 100644 --- a/src/core/renderers/webgl/WebGLRenderer.js +++ b/src/core/renderers/webgl/WebGLRenderer.js @@ -28,11 +28,12 @@ */ export default class WebGLRenderer extends SystemRenderer { + // eslint-disable-next-line valid-jsdoc /** * - * @param {number} [screenWidth=800] - the width of the screen - * @param {number} [screenHeight=600] - the height of the screen * @param {object} [options] - The optional renderer parameters + * @param {number} [options.width=800] - the width of the screen + * @param {number} [options.height=600] - the height of the screen * @param {HTMLCanvasElement} [options.view] - the canvas to use as a view, optional * @param {boolean} [options.transparent=false] - If the render view is transparent, default false * @param {boolean} [options.autoResize=false] - If the render view is automatically resized, default false @@ -52,11 +53,11 @@ * @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(screenWidth, screenHeight, options = {}) + constructor(options, arg2, arg3) { - super('WebGL', screenWidth, screenHeight, options); + super('WebGL', options, arg2, arg3); - this.legacy = !!options.legacy; + this.legacy = this.options.legacy; if (this.legacy) { @@ -85,10 +86,10 @@ */ this._contextOptions = { alpha: this.transparent, - antialias: options.antialias, + antialias: this.options.antialias, premultipliedAlpha: this.transparent && this.transparent !== 'notMultiplied', stencil: true, - preserveDrawingBuffer: options.preserveDrawingBuffer, + preserveDrawingBuffer: this.options.preserveDrawingBuffer, }; this._backgroundColorRgba[3] = this.transparent ? 0 : 1; @@ -129,13 +130,13 @@ * @member {WebGLRenderingContext} */ // initialize the context so it is ready for the managers. - if (options.context) + if (this.options.context) { // checks to see if a context is valid.. - validateContext(options.context); + validateContext(this.options.context); } - this.gl = options.context || glCore.createContext(this.view, this._contextOptions); + this.gl = this.options.context || glCore.createContext(this.view, this._contextOptions); this.CONTEXT_UID = CONTEXT_UID++; @@ -184,6 +185,25 @@ this._nextTextureLocation = 0; this.setBlendMode(0); + + /** + * Fired after rendering finishes. + * + * @event PIXI.WebGLRenderer#postrender + */ + + /** + * Fired before rendering starts. + * + * @event PIXI.WebGLRenderer#prerender + */ + + /** + * Fired when the WebGL context is set. + * + * @event PIXI.WebGLRenderer#context + * @param {WebGLRenderingContext} gl - WebGL context. + */ } /** @@ -706,4 +726,25 @@ } } +/** + * Collection of installed plugins. These are included by default in PIXI, but can be excluded + * by creating a custom build. Consult the README for more information about creating custom + * builds and excluding plugins. + * @name PIXI.WebGLRenderer#plugins + * @type {object} + * @readonly + * @property {PIXI.accessibility.AccessibilityManager} accessibility Support tabbing interactive elements. + * @property {PIXI.extract.WebGLExtract} extract Extract image data from renderer. + * @property {PIXI.interaction.InteractionManager} interaction Handles mouse, touch and pointer events. + * @property {PIXI.prepare.WebGLPrepare} prepare Pre-render display objects. + */ + +/** + * Adds a plugin to the renderer. + * + * @method PIXI.WebGLRenderer#registerPlugin + * @param {string} pluginName - The name of the plugin. + * @param {Function} ctor - The constructor function or class for the plugin. + */ + pluginTarget.mixin(WebGLRenderer); diff --git a/src/core/renderers/webgl/filters/filterTransforms.js b/src/core/renderers/webgl/filters/filterTransforms.js index 5fde3d3..851ad69 100644 --- a/src/core/renderers/webgl/filters/filterTransforms.js +++ b/src/core/renderers/webgl/filters/filterTransforms.js @@ -1,6 +1,6 @@ import { Matrix } from '../../../math'; -/* +/** * Calculates the mapped matrix * @param filterArea {Rectangle} The filter area * @param sprite {Sprite} the target sprite diff --git a/src/core/renderers/webgl/utils/Quad.js b/src/core/renderers/webgl/utils/Quad.js index 218fca5..e8ce543 100644 --- a/src/core/renderers/webgl/utils/Quad.js +++ b/src/core/renderers/webgl/utils/Quad.js @@ -15,7 +15,7 @@ */ constructor(gl, state) { - /* + /** * the current WebGL drawing context * * @member {WebGLRenderingContext} @@ -56,23 +56,31 @@ this.interleaved[(i * 4) + 3] = this.uvs[(i * 2) + 1]; } - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = createIndicesForQuads(1); - /* - * @member {glCore.GLBuffer} The vertex buffer + /** + * The vertex buffer + * + * @member {glCore.GLBuffer} */ this.vertexBuffer = glCore.GLBuffer.createVertexBuffer(gl, this.interleaved, gl.STATIC_DRAW); - /* - * @member {glCore.GLBuffer} The index buffer + /** + * The index buffer + * + * @member {glCore.GLBuffer} */ this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); - /* - * @member {glCore.VertexArrayObject} The index buffer + /** + * The vertex array object + * + * @member {glCore.VertexArrayObject} */ this.vao = new glCore.VertexArrayObject(gl, state); } diff --git a/src/core/settings.js b/src/core/settings.js index f4dd087..d75b058 100644 --- a/src/core/settings.js +++ b/src/core/settings.js @@ -2,6 +2,15 @@ import canUploadSameBuffer from './utils/canUploadSameBuffer'; /** + * User's customizable globals for overriding the default PIXI settings, such + * as a renderer's default resolution, framerate, float percision, etc. + * @example + * // Use the native window resolution as the default resolution + * // will support high-density displays when rendering + * PIXI.settings.RESOLUTION = window.devicePixelRatio. + * + * // Disable interpolation when scaling, will make texture be pixelated + * PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; * @namespace PIXI.settings */ export default { @@ -101,6 +110,9 @@ * @property {boolean} clearBeforeRender=true * @property {boolean} preserveDrawingBuffer=false * @property {boolean} roundPixels=false + * @property {number} width=800 + * @property {number} height=600 + * @property {boolean} legacy=false */ RENDER_OPTIONS: { view: null, @@ -112,6 +124,9 @@ clearBeforeRender: true, preserveDrawingBuffer: false, roundPixels: false, + width: 800, + height: 600, + legacy: false, }, /** diff --git a/src/core/sprites/Sprite.js b/src/core/sprites/Sprite.js index 121bf58..cd574ac 100644 --- a/src/core/sprites/Sprite.js +++ b/src/core/sprites/Sprite.js @@ -147,12 +147,12 @@ // so if _width is 0 then width was not set.. if (this._width) { - this.scale.x = sign(this.scale.x) * this._width / this.texture.orig.width; + this.scale.x = sign(this.scale.x) * this._width / this._texture.orig.width; } if (this._height) { - this.scale.y = sign(this.scale.y) * this._height / this.texture.orig.height; + this.scale.y = sign(this.scale.y) * this._height / this._texture.orig.height; } } diff --git a/src/core/sprites/canvas/CanvasTinter.js b/src/core/sprites/canvas/CanvasTinter.js index 27e5e0c..fd9de5d 100644 --- a/src/core/sprites/canvas/CanvasTinter.js +++ b/src/core/sprites/canvas/CanvasTinter.js @@ -18,7 +18,7 @@ */ getTintedTexture: (sprite, color) => { - const texture = sprite.texture; + const texture = sprite._texture; color = CanvasTinter.roundColor(color); diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 90b08c0..9ddc126 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -115,7 +115,7 @@ this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); } - const shader = this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); + this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); // create a couple of buffers this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); @@ -125,25 +125,27 @@ this.renderer.bindVao(null); + const attrs = this.shader.attributes; + for (let i = 0; i < this.vaoMax; i++) { - this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); - /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[i] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[i] = vao; } this.vao = this.vaos[0]; @@ -379,23 +381,26 @@ if (this.vaoMax <= this.vertexCount) { this.vaoMax++; - this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + + const attrs = this.shader.attributes; /* eslint-disable max-len */ + const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); + /* eslint-enable max-len */ // build the vao object that will render.. - this.vaos[this.vertexCount] = this.renderer.createVao() + const vao = this.renderer.createVao() .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(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) + .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) + .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); - if (this.shader.attributes.aTextureId) + if (attrs.aTextureId) { - this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); } - /* eslint-enable max-len */ + this.vaos[this.vertexCount] = vao; } this.renderer.bindVao(this.vaos[this.vertexCount]); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index cec8c32..c11239a 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 TextMetrics from './TextMetrics'; import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { @@ -42,13 +43,16 @@ canvas.width = 3; canvas.height = 3; - const texture = Texture.fromCanvas(canvas); + const texture = Texture.fromCanvas(canvas, settings.SCALE_MODE, 'text'); texture.orig = new Rectangle(); texture.trim = new Rectangle(); super(texture); + // base texture is already automatically added to the cache, now adding the actual texture + Texture.addToCache(this._texture, this._texture.baseTexture.textureCacheIds[0]); + /** * The canvas element that everything is drawn to * @@ -128,50 +132,18 @@ return; } - this._font = Text.getFontStyle(style); + this._font = this._style.toFontString(); - this.context.font = this._font; - - // word wrap - // preserve original text - const outputText = style.wordWrap ? this.wordWrap(this._text) : this._text; - - // split text into lines - const lines = outputText.split(/(?:\r\n|\r|\n)/); - - // calculate text width - const lineWidths = new Array(lines.length); - let maxLineWidth = 0; - const fontProperties = Text.calculateFontProperties(this._font); - - for (let i = 0; i < lines.length; i++) - { - const lineWidth = this.context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); - - lineWidths[i] = lineWidth; - maxLineWidth = Math.max(maxLineWidth, lineWidth); - } - - let width = maxLineWidth + style.strokeThickness; - - if (style.dropShadow) - { - width += style.dropShadowDistance; - } + const measured = TextMetrics.measureText(this._text, this._style, this._style.wordWrap, this.canvas); + const width = measured.width; + const height = measured.height; + const lines = measured.lines; + const lineHeight = measured.lineHeight; + const lineWidths = measured.lineWidths; + const maxLineWidth = measured.maxLineWidth; + const fontProperties = measured.fontProperties; this.canvas.width = Math.ceil((width + (style.padding * 2)) * this.resolution); - - // calculate text height - const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; - - let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) - + ((lines.length - 1) * lineHeight); - - if (style.dropShadow) - { - height += style.dropShadowDistance; - } - this.canvas.height = Math.ceil((height + (style.padding * 2)) * this.resolution); this.context.scale(this.resolution, this.resolution); @@ -264,12 +236,21 @@ if (style.stroke && style.strokeThickness) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding, true); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding, + true + ); } if (style.fill) { - this.drawLetterSpacing(lines[i], linePositionX + style.padding, linePositionY + style.padding); + this.drawLetterSpacing( + lines[i], + linePositionX + style.padding, + linePositionY + style.padding + ); } } @@ -344,6 +325,7 @@ const texture = this._texture; const style = this._style; + const padding = style.trim ? 0 : style.padding; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; @@ -355,11 +337,11 @@ texture.trim.width = texture._frame.width = this.canvas.width / this.resolution; texture.trim.height = texture._frame.height = this.canvas.height / this.resolution; - texture.trim.x = -style.padding; - texture.trim.y = -style.padding; + texture.trim.x = -padding; + texture.trim.y = -padding; - texture.orig.width = texture._frame.width - (style.padding * 2); - texture.orig.height = texture._frame.height - (style.padding * 2); + texture.orig.width = texture._frame.width - (padding * 2); + texture.orig.height = texture._frame.height - (padding * 2); // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); @@ -407,90 +389,6 @@ } /** - * Applies newlines to a string to have it optimally fit into the horizontal - * bounds set by the Text object's wordWrapWidth property. - * - * @private - * @param {string} text - String to apply word wrapping to - * @return {string} New string with new lines applied where required - */ - wordWrap(text) - { - // Greedy wrapping algorithm that will wrap words as the line grows longer - // than its horizontal bounds. - let result = ''; - const style = this._style; - const lines = text.split('\n'); - const wordWrapWidth = style.wordWrapWidth; - - for (let i = 0; i < lines.length; i++) - { - let spaceLeft = wordWrapWidth; - const words = lines[i].split(' '); - - for (let j = 0; j < words.length; j++) - { - const wordWidth = this.context.measureText(words[j]).width; - - if (style.breakWords && wordWidth > wordWrapWidth) - { - // Word should be split in the middle - const characters = words[j].split(''); - - for (let c = 0; c < characters.length; c++) - { - const characterWidth = this.context.measureText(characters[c]).width; - - if (characterWidth > spaceLeft) - { - result += `\n${characters[c]}`; - spaceLeft = wordWrapWidth - characterWidth; - } - else - { - if (c === 0) - { - result += ' '; - } - - result += characters[c]; - spaceLeft -= characterWidth; - } - } - } - else - { - const wordWidthWithSpace = wordWidth + this.context.measureText(' ').width; - - if (j === 0 || wordWidthWithSpace > spaceLeft) - { - // Skip printing the newline if it's the first word of the line that is - // greater than the word wrap width. - if (j > 0) - { - result += '\n'; - } - result += words[j]; - spaceLeft = wordWrapWidth - wordWidth; - } - else - { - spaceLeft -= wordWidthWithSpace; - result += ` ${words[j]}`; - } - } - } - - if (i < lines.length - 1) - { - result += '\n'; - } - } - - return result; - } - - /** * Gets the local bounds of the text object. * * @param {Rectangle} rect - The output rectangle. @@ -755,157 +653,4 @@ this._text = text; this.dirty = true; } - - /** - * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter - * as Text.style. - * - * @static - * @param {object|TextStyle} style - String representing the style of the font - * @return {string} Font style string, for passing to Text.calculateFontProperties() - */ - static getFontStyle(style) - { - style = style || {}; - - if (!(style instanceof TextStyle)) - { - style = new TextStyle(style); - } - - // build canvas api font setting from individual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - // Clean-up fontFamily property by quoting each font name - // this will support font names with spaces - let fontFamilies = style.fontFamily; - - if (!Array.isArray(style.fontFamily)) - { - fontFamilies = style.fontFamily.split(','); - } - - for (let i = fontFamilies.length - 1; i >= 0; i--) - { - // Trim any extra white-space - let fontFamily = fontFamilies[i].trim(); - - // Check if font already contains strings - if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) - { - fontFamily = `"${fontFamily}"`; - } - fontFamilies[i] = fontFamily; - } - - return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; - } - - /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @static - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - static calculateFontProperties(fontStyle) - { - // as this method is used for preparing assets, don't recalculate things if we don't need to - if (Text.fontPropertiesCache[fontStyle]) - { - return Text.fontPropertiesCache[fontStyle]; - } - - const properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - - return properties; - } } - -Text.fontPropertiesCache = {}; -Text.fontPropertiesCanvas = document.createElement('canvas'); -Text.fontPropertiesContext = Text.fontPropertiesCanvas.getContext('2d'); diff --git a/src/core/text/TextMetrics.js b/src/core/text/TextMetrics.js new file mode 100644 index 0000000..27bd145 --- /dev/null +++ b/src/core/text/TextMetrics.js @@ -0,0 +1,327 @@ +/** + * The TextMetrics object represents the measurement of a block of text with a specified style. + * + * @class + * @memberOf PIXI + */ +export default class TextMetrics +{ + /** + * @param {string} text - the text that was measured + * @param {PIXI.TextStyle} style - the style that was measured + * @param {number} width - the measured width of the text + * @param {number} height - the measured height of the text + * @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style + * @param {array} lineWidths - an array of the line widths for each line matched to `lines` + * @param {number} lineHeight - the measured line height for this style + * @param {number} maxLineWidth - the maximum line width for all measured lines + * @param {Object} fontProperties - the font properties object from TextMetrics.measureFont + */ + constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties) + { + this.text = text; + this.style = style; + this.width = width; + this.height = height; + this.lines = lines; + this.lineWidths = lineWidths; + this.lineHeight = lineHeight; + this.maxLineWidth = maxLineWidth; + this.fontProperties = fontProperties; + } + + /** + * Measures the supplied string of text and returns a Rectangle. + * + * @param {string} text - the text to measure. + * @param {PIXI.TextStyle} style - the text style to use for measuring + * @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text. + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {PIXI.TextMetrics} measured width and height of the text. + */ + static measureText(text, style, wordWrap, canvas = TextMetrics._canvas) + { + wordWrap = wordWrap || style.wordWrap; + const font = style.toFontString(); + const fontProperties = TextMetrics.measureFont(font); + const context = canvas.getContext('2d'); + + context.font = font; + + const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text; + const lines = outputText.split(/(?:\r\n|\r|\n)/); + const lineWidths = new Array(lines.length); + let maxLineWidth = 0; + + for (let i = 0; i < lines.length; i++) + { + const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing); + + lineWidths[i] = lineWidth; + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + let width = maxLineWidth + style.strokeThickness; + + if (style.dropShadow) + { + width += style.dropShadowDistance; + } + + const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness; + let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness) + + ((lines.length - 1) * lineHeight); + + if (style.dropShadow) + { + height += style.dropShadowDistance; + } + + return new TextMetrics( + text, + style, + width, + height, + lines, + lineWidths, + lineHeight, + maxLineWidth, + fontProperties + ); + } + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + * + * @private + * @param {string} text - String to apply word wrapping to + * @param {PIXI.TextStyle} style - the style to use when wrapping + * @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring. + * @return {string} New string with new lines applied where required + */ + static wordWrap(text, style, canvas = TextMetrics._canvas) + { + const context = canvas.getContext('2d'); + + // Greedy wrapping algorithm that will wrap words as the line grows longer + // than its horizontal bounds. + let result = ''; + const lines = text.split('\n'); + const wordWrapWidth = style.wordWrapWidth; + const characterCache = {}; + + for (let i = 0; i < lines.length; i++) + { + let spaceLeft = wordWrapWidth; + const words = lines[i].split(' '); + + for (let j = 0; j < words.length; j++) + { + const wordWidth = context.measureText(words[j]).width; + + if (style.breakWords && wordWidth > wordWrapWidth) + { + // Word should be split in the middle + const characters = words[j].split(''); + + for (let c = 0; c < characters.length; c++) + { + const character = characters[c]; + let characterWidth = characterCache[character]; + + if (characterWidth === undefined) + { + characterWidth = context.measureText(character).width; + characterCache[character] = characterWidth; + } + + if (characterWidth > spaceLeft) + { + result += `\n${character}`; + spaceLeft = wordWrapWidth - characterWidth; + } + else + { + if (c === 0) + { + result += ' '; + } + + result += character; + spaceLeft -= characterWidth; + } + } + } + else + { + const wordWidthWithSpace = wordWidth + context.measureText(' ').width; + + if (j === 0 || wordWidthWithSpace > spaceLeft) + { + // Skip printing the newline if it's the first word of the line that is + // greater than the word wrap width. + if (j > 0) + { + result += '\n'; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else + { + spaceLeft -= wordWidthWithSpace; + result += ` ${words[j]}`; + } + } + } + + if (i < lines.length - 1) + { + result += '\n'; + } + } + + return result; + } + + /** + * Calculates the ascent, descent and fontSize of a given font-style + * + * @static + * @param {string} font - String representing the style of the font + * @return {PIXI.TextMetrics~FontMetrics} Font properties object + */ + static measureFont(font) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (TextMetrics._fonts[font]) + { + return TextMetrics._fonts[font]; + } + + const properties = {}; + + const canvas = TextMetrics._canvas; + const context = TextMetrics._context; + + context.font = font; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = font; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + TextMetrics._fonts[font] = properties; + + return properties; + } +} + +/** + * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}. + * @class FontMetrics + * @memberof PIXI.TextMetrics~ + * @property {number} ascent - The ascent distance + * @property {number} descent - The descent distance + * @property {number} fontSize - Font size from ascent to descent + */ + +const canvas = document.createElement('canvas'); + +canvas.width = canvas.height = 10; + +/** + * Cached canvas element for measuring text + * @memberof PIXI.TextMetrics + * @type {HTMLCanvasElement} + * @private + */ +TextMetrics._canvas = canvas; + +/** + * Cache for context to use. + * @memberof PIXI.TextMetrics + * @type {CanvasRenderingContext2D} + * @private + */ +TextMetrics._context = canvas.getContext('2d'); + +/** + * Cache of PIXI.TextMetrics~FontMetrics objects. + * @memberof PIXI.TextMetrics + * @type {Object} + * @private + */ +TextMetrics._fonts = {}; diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 890950b..4d66953 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -473,6 +473,41 @@ this.styleID++; } } + + /** + * Generates a font style string to use for `TextMetrics.measureFont()`. + * + * @return {string} Font style string, for passing to `TextMetrics.measureFont()` + */ + toFontString() + { + // build canvas api font setting from individual components. Convert a numeric this.fontSize to px + const fontSizeString = (typeof this.fontSize === 'number') ? `${this.fontSize}px` : this.fontSize; + + // Clean-up fontFamily property by quoting each font name + // this will support font names with spaces + let fontFamilies = this.fontFamily; + + if (!Array.isArray(this.fontFamily)) + { + fontFamilies = this.fontFamily.split(','); + } + + for (let i = fontFamilies.length - 1; i >= 0; i--) + { + // Trim any extra white-space + let fontFamily = fontFamilies[i].trim(); + + // Check if font already contains strings + if (!(/([\"\'])[^\'\"]+\1/).test(fontFamily)) + { + fontFamily = `"${fontFamily}"`; + } + fontFamilies[i] = fontFamily; + } + + return `${this.fontStyle} ${this.fontVariant} ${this.fontWeight} ${fontSizeString} ${fontFamilies.join(',')}`; + } } /** diff --git a/src/core/textures/BaseTexture.js b/src/core/textures/BaseTexture.js index bbee141..094ad8b 100644 --- a/src/core/textures/BaseTexture.js +++ b/src/core/textures/BaseTexture.js @@ -196,12 +196,6 @@ this._enabled = 0; this._virtalBoundId = -1; - // if no source passed don't try to load - if (source) - { - this.loadSource(source); - } - /** * If the object has been destroyed via destroy(). If true, it should not be used. * @@ -212,26 +206,57 @@ this._destroyed = false; /** + * The ids under which this BaseTexture has been added to the base texture cache. This is + * automatically set as long as BaseTexture.addToCache is used, but may not be set if a + * BaseTexture is added directly to the BaseTextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; + + // if no source passed don't try to load + if (source) + { + this.loadSource(source); + } + + /** * Fired when a not-immediately-available source finishes loading. * * @protected - * @event loaded - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#loaded + * @param {PIXI.BaseTexture} baseTexture - Resource loaded. */ /** * Fired when a not-immediately-available source fails to load. * * @protected - * @event error - * @memberof PIXI.BaseTexture# + * @event PIXI.BaseTexture#error + * @param {PIXI.BaseTexture} baseTexture - Resource errored. + */ + + /** + * Fired when BaseTexture is updated. + * + * @protected + * @event PIXI.BaseTexture#update + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being updated. + */ + + /** + * Fired when BaseTexture is destroyed. + * + * @protected + * @event PIXI.BaseTexture#dispose + * @param {PIXI.BaseTexture} baseTexture - Instance of texture being destroyed. */ } /** * Updates the texture on all the webgl renderers, this also assumes the src has changed. * - * @fires update + * @fires PIXI.BaseTexture#update */ update() { @@ -524,7 +549,7 @@ * * @param {string} svgString SVG source as string * - * @fires loaded + * @fires PIXI.BaseTexture#loaded */ _loadSvgSourceUsingString(svgString) { @@ -561,7 +586,7 @@ this.source = canvas; // Add also the canvas in cache (destroy clears by `imageUrl` and `source._pixiId`) - BaseTextureCache[canvas._pixiId] = this; + BaseTexture.addToCache(this, canvas._pixiId); this.isLoading = false; this._sourceLoaded(); @@ -588,7 +613,6 @@ { if (this.imageUrl) { - delete BaseTextureCache[this.imageUrl]; delete TextureCache[this.imageUrl]; this.imageUrl = null; @@ -598,16 +622,14 @@ this.source.src = ''; } } - // An svg source has both `imageUrl` and `__pixiId`, so no `else if` here - if (this.source && this.source._pixiId) - { - delete BaseTextureCache[this.source._pixiId]; - } this.source = null; this.dispose(); + BaseTexture.removeFromCache(this); + this.textureCacheIds = null; + this._destroyed = true; } @@ -616,6 +638,7 @@ * This means you can still use the texture later which will upload it to GPU * memory again. * + * @fires PIXI.BaseTexture#dispose */ dispose() { @@ -674,7 +697,7 @@ image.src = imageUrl; // Setting this triggers load - BaseTextureCache[imageUrl] = baseTexture; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -686,13 +709,14 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.BaseTexture} The new base texture. */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { if (!canvas._pixiId) { - canvas._pixiId = `canvas_${uid()}`; + canvas._pixiId = `${origin}_${uid()}`; } let baseTexture = BaseTextureCache[canvas._pixiId]; @@ -700,7 +724,7 @@ if (!baseTexture) { baseTexture = new BaseTexture(canvas, scaleMode); - BaseTextureCache[canvas._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, canvas._pixiId); } return baseTexture; @@ -740,7 +764,7 @@ // 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; + BaseTexture.addToCache(baseTexture, imageUrl); } return baseTexture; @@ -753,4 +777,75 @@ // lets assume its a base texture! return source; } + + /** + * Adds a BaseTexture to the global BaseTextureCache. This cache is shared across the whole PIXI object. + * + * @static + * @param {PIXI.BaseTexture} baseTexture - The BaseTexture to add to the cache. + * @param {string} id - The id that the BaseTexture will be stored against. + */ + static addToCache(baseTexture, id) + { + if (id) + { + if (baseTexture.textureCacheIds.indexOf(id) === -1) + { + baseTexture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (BaseTextureCache[id]) + { + console.warn(`BaseTexture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + BaseTextureCache[id] = baseTexture; + } + } + + /** + * Remove a BaseTexture from the global BaseTextureCache. + * + * @static + * @param {string|PIXI.BaseTexture} baseTexture - id of a BaseTexture to be removed, or a BaseTexture instance itself. + * @return {PIXI.BaseTexture|null} The BaseTexture that was removed. + */ + static removeFromCache(baseTexture) + { + if (typeof baseTexture === 'string') + { + const baseTextureFromCache = BaseTextureCache[baseTexture]; + + if (baseTextureFromCache) + { + const index = baseTextureFromCache.textureCacheIds.indexOf(baseTexture); + + if (index > -1) + { + baseTextureFromCache.textureCacheIds.splice(index, 1); + } + + delete BaseTextureCache[baseTexture]; + + return baseTextureFromCache; + } + } + else + { + for (let i = 0; i < baseTexture.textureCacheIds.length; ++i) + { + delete BaseTextureCache[baseTexture.textureCacheIds[i]]; + } + + baseTexture.textureCacheIds.length = 0; + + return baseTexture; + } + + return null; + } } diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js index d469c62..85abccf 100644 --- a/src/core/textures/Spritesheet.js +++ b/src/core/textures/Spritesheet.js @@ -1,5 +1,5 @@ import { Rectangle, Texture } from '../'; -import { getResolutionOfUrl, TextureCache } from '../utils'; +import { getResolutionOfUrl } from '../utils'; /** * Utility class for maintaining reference to a collection @@ -207,7 +207,7 @@ ); // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - TextureCache[i] = this.textures[i]; + Texture.addToCache(this.textures[i], i); } frameIndex++; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 133a59b..6c4d324 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,8 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; +import { TextureCache, getResolutionOfUrl } from '../utils'; +import settings from '../settings'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -144,9 +145,9 @@ /** * Fired when the texture is updated. This happens if the frame or the baseTexture is updated. * - * @event update - * @memberof PIXI.Texture# + * @event PIXI.Texture#update * @protected + * @param {PIXI.Texture} texture - Instance of texture being updated. */ this._updateID = 0; @@ -156,6 +157,15 @@ * @type {Object} */ this.transform = null; + + /** + * The ids under which this Texture has been added to the texture cache. This is + * automatically set as long as Texture.addToCache is used, but may not be set if a + * Texture is added directly to the TextureCache array. + * + * @member {string[]} + */ + this.textureCacheIds = []; } /** @@ -222,7 +232,7 @@ // this only needs to be removed if the base texture is actually destroyed too.. if (TextureCache[this.baseTexture.imageUrl]) { - delete TextureCache[this.baseTexture.imageUrl]; + Texture.removeFromCache(this.baseTexture.imageUrl); } this.baseTexture.destroy(); @@ -241,8 +251,8 @@ this.valid = false; - this.off('dispose', this.dispose, this); - this.off('update', this.update, this); + Texture.removeFromCache(this); + this.textureCacheIds = null; } /** @@ -290,7 +300,7 @@ if (!texture) { texture = new Texture(BaseTexture.fromImage(imageUrl, crossorigin, scaleMode, sourceScale)); - TextureCache[imageUrl] = texture; + Texture.addToCache(texture, imageUrl); } return texture; @@ -322,11 +332,12 @@ * @static * @param {HTMLCanvasElement} canvas - The canvas element source of the texture * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {string} [origin='canvas'] - A string origin of who created the base texture * @return {PIXI.Texture} The newly created texture */ - static fromCanvas(canvas, scaleMode) + static fromCanvas(canvas, scaleMode, origin = 'canvas') { - return new Texture(BaseTexture.fromCanvas(canvas, scaleMode)); + return new Texture(BaseTexture.fromCanvas(canvas, scaleMode, origin)); } /** @@ -398,7 +409,7 @@ } else if (source instanceof HTMLCanvasElement) { - return Texture.fromCanvas(source); + return Texture.fromCanvas(source, settings.SCALE_MODE, 'HTMLCanvasElement'); } else if (source instanceof HTMLVideoElement) { @@ -437,46 +448,88 @@ } // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - BaseTextureCache[name] = baseTexture; - TextureCache[name] = texture; + BaseTexture.addToCache(texture.baseTexture, name); + Texture.addToCache(texture, name); // also add references by url if they are different. if (name !== imageUrl) { - BaseTextureCache[imageUrl] = baseTexture; - TextureCache[imageUrl] = texture; + BaseTexture.addToCache(texture.baseTexture, imageUrl); + Texture.addToCache(texture, imageUrl); } return texture; } /** - * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. + * Adds a Texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static * @param {PIXI.Texture} texture - The Texture to add to the cache. - * @param {string} id - The id that the texture will be stored against. + * @param {string} id - The id that the Texture will be stored against. */ - static addTextureToCache(texture, id) + static addToCache(texture, id) { - TextureCache[id] = texture; + if (id) + { + if (texture.textureCacheIds.indexOf(id) === -1) + { + texture.textureCacheIds.push(id); + } + + // @if DEBUG + /* eslint-disable no-console */ + if (TextureCache[id]) + { + console.warn(`Texture added to the cache with an id [${id}] that already had an entry`); + } + /* eslint-enable no-console */ + // @endif + + TextureCache[id] = texture; + } } /** - * Remove a texture from the global TextureCache. + * Remove a Texture from the global TextureCache. * * @static - * @param {string} id - The id of the texture to be removed - * @return {PIXI.Texture} The texture that was removed + * @param {string|PIXI.Texture} texture - id of a Texture to be removed, or a Texture instance itself + * @return {PIXI.Texture|null} The Texture that was removed */ - static removeTextureFromCache(id) + static removeFromCache(texture) { - const texture = TextureCache[id]; + if (typeof texture === 'string') + { + const textureFromCache = TextureCache[texture]; - delete TextureCache[id]; - delete BaseTextureCache[id]; + if (textureFromCache) + { + const index = textureFromCache.textureCacheIds.indexOf(texture); - return texture; + if (index > -1) + { + textureFromCache.textureCacheIds.splice(index, 1); + } + + delete TextureCache[texture]; + + return textureFromCache; + } + } + else + { + for (let i = 0; i < texture.textureCacheIds.length; ++i) + { + delete TextureCache[texture.textureCacheIds[i]]; + } + + texture.textureCacheIds.length = 0; + + return texture; + } + + return null; } /** diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..65b74b9 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,12 +188,12 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) { - delete BaseTextureCache[this.source._pixiId]; + BaseTexture.removeFromCache(this.source._pixiId); delete this.source._pixiId; } @@ -219,7 +220,7 @@ if (!baseTexture) { baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTextureCache[video._pixiId] = baseTexture; + BaseTexture.addToCache(baseTexture, video._pixiId); } return baseTexture; @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..40df138 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around listeners * meant for execution on the next requested animation frame. * Animation frames are requested only when necessary, * e.g. When the ticker is started and the emitter has listeners. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#on} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * Calls {@link module:eventemitter3.EventEmitter#once} internally for the - * internal 'tick' event. It checks if the emitter has listeners, - * and if so it requests a new animation frame at this point. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event. - * It checks if the emitter has listeners for 'tick' event. - * If it does, then it cancels the animation frame. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -320,8 +389,22 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; + // Cache a local reference, in-case ticker is destroyed + // during the emit, we can still check for head.next + const head = this._head; + // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..c3020d4 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,8 +45,25 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** + * This namespace contains an API for interacting with PIXI's internal global update loop. + * + * This ticker is used for rendering, {@link PIXI.extras.AnimatedSprite AnimatedSprite}, + * {@link PIXI.interaction.InteractionManager InteractionManager} and many other time-based PIXI systems. + * @example + * const ticker = new PIXI.ticker.Ticker(); + * ticker.stop(); + * ticker.add((deltaTime) => { + * // do something every frame + * }); + * ticker.start(); * @namespace PIXI.ticker */ export { shared, Ticker }; diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 99e585a..098b383 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -9,6 +9,21 @@ let saidHello = false; /** + * Generalized convenience utilities for PIXI. + * @example + * // Extend PIXI's internal Event Emitter. + * class MyEmitter extends PIXI.utils.EventEmitter { + * constructor() { + * super(); + * console.log("Emitter created!"); + * } + * } + * + * // Get info on current device + * console.log(PIXI.utils.isMobile); + * + * // Convert hex color to string + * console.log(PIXI.utils.hex2string(0xff00ff)); // returns: "#ff00ff" * @namespace PIXI.utils */ export { @@ -353,7 +368,7 @@ * @memberof PIXI.utils * @private */ -export const TextureCache = {}; +export const TextureCache = Object.create(null); /** * @todo Describe property usage @@ -361,7 +376,7 @@ * @memberof PIXI.utils * @private */ -export const BaseTextureCache = {}; +export const BaseTextureCache = Object.create(null); /** * Destroys all texture in the cache diff --git a/src/deprecation.js b/src/deprecation.js index b9c73b3..0a9a771 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -695,6 +695,49 @@ }; /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @name PIXI.Text.calculateFontProperties + * @see PIXI.TextMetrics.measureFont + * @deprecated since version 4.5.0 + * @param {string} font - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.calculateFontProperties = function calculateFontProperties(font) +{ + warn(`Text.calculateFontProperties is now deprecated, please use the TextMetrics.measureFont`); + + return core.TextMetrics.measureFont(font); +}; + +Object.defineProperties(core.Text, { + fontPropertiesCache: { + get() + { + warn(`Text.fontPropertiesCache is deprecated`); + + return core.TextMetrics._fonts; + }, + }, + fontPropertiesCanvas: { + get() + { + warn(`Text.fontPropertiesCanvas is deprecated`); + + return core.TextMetrics._canvas; + }, + }, + fontPropertiesContext: { + get() + { + warn(`Text.fontPropertiesContext is deprecated`); + + return core.TextMetrics._context; + }, + }, +}); + +/** * @method * @name PIXI.Text#setStyle * @see PIXI.Text#style @@ -710,7 +753,7 @@ /** * @method * @name PIXI.Text#determineFontProperties - * @see PIXI.Text#calculateFontProperties + * @see PIXI.Text#measureFontProperties * @deprecated since version 4.2.0 * @private * @param {string} fontStyle - String representing the style of the font @@ -718,10 +761,31 @@ */ core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) { - warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' - + 'e.g : Text.calculateFontProperties(fontStyle);'); + warn('determineFontProperties is now deprecated, please use TextMetrics.measureFont method'); - return core.Text.calculateFontProperties(fontStyle); + return core.TextMetrics.measureFont(fontStyle); +}; + +/** + * @method + * @name PIXI.Text.getFontStyle + * @see PIXI.TextMetrics.getFontStyle + * @deprecated since version 4.5.0 + * @param {PIXI.TextStyle} style - The style to use. + * @return {string} Font string + */ +core.Text.getFontStyle = function getFontStyle(style) +{ + warn('getFontStyle is now deprecated, please use TextStyle.toFontString() instead'); + + style = style || {}; + + if (!(style instanceof core.TextStyle)) + { + style = new core.TextStyle(style); + } + + return style.toFontString(); }; Object.defineProperties(core.TextStyle.prototype, { @@ -830,6 +894,41 @@ warn('setFrame is now deprecated, please use the frame property, e.g: myTexture.frame = frame;'); }; +/** + * @static + * @function + * @name PIXI.Texture.addTextureToCache + * @see PIXI.Texture.addToCache + * @deprecated since 4.5.0 + * @param {PIXI.Texture} texture - The Texture to add to the cache. + * @param {string} id - The id that the texture will be stored against. + */ +core.Texture.addTextureToCache = function addTextureToCache(texture, id) +{ + core.Texture.addToCache(texture, id); + warn('Texture.addTextureToCache is deprecated, please use Texture.addToCache from now on.'); +}; + +/** + * @static + * @function + * @name PIXI.Texture.removeTextureFromCache + * @see PIXI.Texture.removeFromCache + * @deprecated since 4.5.0 + * @param {string} id - The id of the texture to be removed + * @return {PIXI.Texture|null} The texture that was removed + */ +core.Texture.removeTextureFromCache = function removeTextureFromCache(id) +{ + warn('Texture.removeTextureFromCache is deprecated, please use Texture.removeFromCache from now on. ' + + 'Be aware that Texture.removeFromCache does not automatically its BaseTexture from the BaseTextureCache. ' + + 'For that, use BaseTexture.removeFromCache'); + + core.BaseTexture.removeFromCache(id); + + return core.Texture.removeFromCache(id); +}; + Object.defineProperties(filters, { /** @@ -918,6 +1017,35 @@ }); /** + * @method + * @name PIXI.prepare.BasePrepare#register + * @see PIXI.prepare.BasePrepare#registerFindHook + * @deprecated since version 4.4.2 + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ +prepare.BasePrepare.prototype.register = function register(addHook, uploadHook) +{ + warn('renderer.plugins.prepare.register is now deprecated, ' + + 'please use renderer.plugins.prepare.registerFindHook & renderer.plugins.prepare.registerUploadHook'); + + if (addHook) + { + this.registerFindHook(addHook); + } + + if (uploadHook) + { + this.registerUploadHook(uploadHook); + } + + return this; +}; + +/** * The number of graphics or textures to upload to the GPU. * * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME @@ -969,67 +1097,73 @@ }, }); -Object.defineProperties(loaders.Resource.prototype, { - isJson: { - get() - { - warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); +if (loaders.Loader) +{ + const Resource = loaders.Resource; + const Loader = loaders.Loader; - return this.type === loaders.Loader.Resource.TYPE.JSON; + Object.defineProperties(Resource.prototype, { + isJson: { + get() + { + warn('The isJson property is deprecated, please use `resource.type === Resource.TYPE.JSON`.'); + + return this.type === Resource.TYPE.JSON; + }, }, - }, - isXml: { - get() - { - warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); + isXml: { + get() + { + warn('The isXml property is deprecated, please use `resource.type === Resource.TYPE.XML`.'); - return this.type === loaders.Loader.Resource.TYPE.XML; + return this.type === Resource.TYPE.XML; + }, }, - }, - isImage: { - get() - { - warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); + isImage: { + get() + { + warn('The isImage property is deprecated, please use `resource.type === Resource.TYPE.IMAGE`.'); - return this.type === loaders.Loader.Resource.TYPE.IMAGE; + return this.type === Resource.TYPE.IMAGE; + }, }, - }, - isAudio: { - get() - { - warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); + isAudio: { + get() + { + warn('The isAudio property is deprecated, please use `resource.type === Resource.TYPE.AUDIO`.'); - return this.type === loaders.Loader.Resource.TYPE.AUDIO; + return this.type === Resource.TYPE.AUDIO; + }, }, - }, - isVideo: { - get() - { - warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); + isVideo: { + get() + { + warn('The isVideo property is deprecated, please use `resource.type === Resource.TYPE.VIDEO`.'); - return this.type === loaders.Loader.Resource.TYPE.VIDEO; + return this.type === Resource.TYPE.VIDEO; + }, }, - }, -}); + }); -Object.defineProperties(loaders.Loader.prototype, { - before: { - get() - { - warn('The before() method is deprecated, please use pre().'); + Object.defineProperties(Loader.prototype, { + before: { + get() + { + warn('The before() method is deprecated, please use pre().'); - return this.pre; + return this.pre; + }, }, - }, - after: { - get() - { - warn('The after() method is deprecated, please use use().'); + after: { + get() + { + warn('The after() method is deprecated, please use use().'); - return this.use; + return this.use; + }, }, - }, -}); + }); +} /** * @name PIXI.interaction.interactiveTarget#defaultCursor diff --git a/src/extract/canvas/CanvasExtract.js b/src/extract/canvas/CanvasExtract.js index 941ca05..3ac5aee 100644 --- a/src/extract/canvas/CanvasExtract.js +++ b/src/extract/canvas/CanvasExtract.js @@ -8,7 +8,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class CanvasExtract { @@ -21,9 +21,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.CanvasExtract} extract + * @member {PIXI.extract.CanvasExtract} extract * @memberof PIXI.CanvasRenderer# - * @see PIXI.CanvasExtract + * @see PIXI.extract.CanvasExtract */ renderer.extract = this; } diff --git a/src/extract/index.js b/src/extract/index.js index 0f2170d..e9478c1 100644 --- a/src/extract/index.js +++ b/src/extract/index.js @@ -1,2 +1,22 @@ +/** + * This namespace provides renderer-specific plugins for exporting content from a renderer. + * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels). + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new app (will auto-add extract plugin to renderer) + * const app = new PIXI.Application(); + * + * // Draw a red circle + * const graphics = new PIXI.Graphics() + * .beginFill(0xFF0000) + * .drawCircle(0, 0, 50); + * + * // Render the graphics as an HTMLImageElement + * const image = app.renderer.plugins.extract.image(graphics); + * document.body.appendChild(image); + * @namespace PIXI.extract + */ export { default as webgl } from './webgl/WebGLExtract'; export { default as canvas } from './canvas/CanvasExtract'; diff --git a/src/extract/webgl/WebGLExtract.js b/src/extract/webgl/WebGLExtract.js index 60228bb..1c3dd30 100644 --- a/src/extract/webgl/WebGLExtract.js +++ b/src/extract/webgl/WebGLExtract.js @@ -9,7 +9,7 @@ * An instance of this class is automatically created by default, and can be found at renderer.plugins.extract * * @class - * @memberof PIXI + * @memberof PIXI.extract */ export default class WebGLExtract { @@ -22,9 +22,9 @@ /** * Collection of methods for extracting data (image, pixels, etc.) from a display object or render texture * - * @member {PIXI.WebGLExtract} extract + * @member {PIXI.extract.WebGLExtract} extract * @memberof PIXI.WebGLRenderer# - * @see PIXI.WebGLExtract + * @see PIXI.extract.WebGLExtract */ renderer.extract = this; } diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 78e6e40..16536d2 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -65,8 +65,36 @@ } /** + * Multiplies uvs array to transform + * @param {Float32Array} uvs mesh uvs + * @param {Float32Array} [out=uvs] output + * @returns {Float32Array} output + */ + multiplyUvs(uvs, out) + { + if (out === undefined) + { + out = uvs; + } + + const mat = this.mapCoord; + + for (let i = 0; i < uvs.length; i += 2) + { + const x = uvs[i]; + const y = uvs[i + 1]; + + out[i] = (x * mat.a) + (y * mat.c) + mat.tx; + out[i + 1] = (x * mat.b) + (y * mat.d) + mat.ty; + } + + return out; + } + + /** * updates matrices if texture was changed * @param {boolean} forceUpdate if true, matrices will be updated any case + * @returns {boolean} whether or not it was updated */ update(forceUpdate) { @@ -74,13 +102,13 @@ if (!tex || !tex.valid) { - return; + return false; } if (!forceUpdate && this._lastTextureID === tex._updateID) { - return; + return false; } this._lastTextureID = tex._updateID; @@ -110,5 +138,7 @@ frame[3] = (tex._frame.y + tex._frame.height - margin + offset) / texBase.height; this.uClampOffset[0] = offset / texBase.realWidth; this.uClampOffset[1] = offset / texBase.realHeight; + + return true; } } diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index fe82451..f31c2b6 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -180,9 +180,9 @@ const transform = this.worldTransform; const resolution = renderer.resolution; const baseTexture = texture.baseTexture; - const baseTextureResolution = texture.baseTexture.resolution; - const modX = (this.tilePosition.x / this.tileScale.x) % texture._frame.width; - const modY = (this.tilePosition.y / this.tileScale.y) % texture._frame.height; + const baseTextureResolution = baseTexture.resolution; + const modX = ((this.tilePosition.x / this.tileScale.x) % texture._frame.width) * baseTextureResolution; + const modY = ((this.tilePosition.y / this.tileScale.y) % texture._frame.height) * baseTextureResolution; // create a nice shiny pattern! // TODO this needs to be refreshed if texture changes.. diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 37e55d2..4a165a3 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -1,4 +1,7 @@ import * as core from '../core'; +import Texture from '../core/textures/Texture'; +import BaseTexture from '../core/textures/BaseTexture'; +import { uid } from '../core/utils'; const DisplayObject = core.DisplayObject; const _tempMatrix = new core.Matrix(); @@ -20,6 +23,8 @@ */ constructor() { + this.textureCacheId = null; + this.originalRenderWebGL = null; this.originalRenderCanvas = null; this.originalCalculateBounds = null; @@ -185,6 +190,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -227,7 +239,16 @@ this.transform._parentID = -1; // restore the transform of the cached sprite to avoid the nasty flicker.. - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } // map the hit test.. this.containsPoint = cachedSprite.containsPoint.bind(cachedSprite); @@ -280,6 +301,13 @@ const renderTexture = core.RenderTexture.create(bounds.width | 0, bounds.height | 0); + const textureCacheId = `cacheAsBitmap_${uid()}`; + + this._cacheData.textureCacheId = textureCacheId; + + BaseTexture.addToCache(renderTexture.baseTexture, textureCacheId); + Texture.addToCache(renderTexture, textureCacheId); + // need to set // const m = _tempMatrix; @@ -314,7 +342,17 @@ cachedSprite._bounds = this._bounds; cachedSprite.alpha = cacheAlpha; - this.updateTransform(); + if (!this.parent) + { + this.parent = renderer._tempDisplayObjectParent; + this.updateTransform(); + this.parent = null; + } + else + { + this.updateTransform(); + } + this.updateTransform = this.displayObjectUpdateTransform; this._cacheData.sprite = cachedSprite; @@ -352,6 +390,11 @@ { this._cacheData.sprite._texture.destroy(true); this._cacheData.sprite = null; + + BaseTexture.removeFromCache(this._cacheData.textureCacheId); + Texture.removeFromCache(this._cacheData.textureCacheId); + + this._cacheData.textureCacheId = null; }; /** diff --git a/src/extras/index.js b/src/extras/index.js index bbd8bbf..a34d7ec 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -1,7 +1,7 @@ /** + * Additional PIXI DisplayObjects for animation, tiling and bitmap text. * @namespace PIXI.extras */ -export { default as TextureTransform } from './TextureTransform'; export { default as AnimatedSprite } from './AnimatedSprite'; export { default as TilingSprite } from './TilingSprite'; export { default as TilingSpriteRenderer } from './webgl/TilingSpriteRenderer'; diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index 2591719..5734fc0 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -10,7 +10,7 @@ * WebGL renderer plugin for tiling sprites * * @class - * @memberof PIXI + * @memberof PIXI.extras * @extends PIXI.ObjectRenderer */ export default class TilingSpriteRenderer extends core.ObjectRenderer @@ -131,7 +131,7 @@ tempMat.invert(); if (isSimple) { - tempMat.append(uv.mapCoord); + tempMat.prepend(uv.mapCoord); } else { diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index d5355c3..b5d030c 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -35,7 +35,7 @@ this.maskSprite = sprite; this.maskMatrix = maskMatrix; - this.uniforms.mapSampler = sprite.texture; + this.uniforms.mapSampler = sprite._texture; this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; diff --git a/src/filters/index.js b/src/filters/index.js index f3e8862..7ceb50a 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,4 +1,21 @@ /** + * This namespace contains WebGL-only display filters that can be applied + * to DisplayObjects using the {@link PIXI.DisplayObject#filters filters} property. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * + * // Draw a green rectangle + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add a blur filter + * rect.filters = [new PIXI.filters.BlurFilter()]; + * + * // Display rectangle + * app.stage.addChild(rect); + * document.body.appendChild(app.view); * @namespace PIXI.filters */ export { default as FXAAFilter } from './fxaa/FXAAFilter'; diff --git a/src/index.js b/src/index.js index c553427..77da490 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,14 @@ import { utils } from './core'; utils.mixins.performMixins(); +/** + * Alias for {@link PIXI.loaders.shared}. + * @name loader + * @memberof PIXI + * @type {PIXI.loader.Loader} + */ +const loader = loaders.shared || null; + export { accessibility, extract, @@ -30,18 +38,8 @@ mesh, particles, prepare, + loader, }; -/** - * A premade instance of the loader that can be used to load resources. - * - * @name loader - * @memberof PIXI - * @property {PIXI.loaders.Loader} - */ -const loader = loaders && loaders.Loader ? new loaders.Loader() : null; // check is there in case user excludes loader lib - -export { loader }; - // Always export pixi globally. global.PIXI = exports; // eslint-disable-line diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 818da35..f4619a1 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -30,7 +30,10 @@ /** * When passed to an event handler, this will be the original DOM Event that was captured * - * @member {Event} + * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent + * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index dc554f9..a7be332 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -33,14 +33,14 @@ */ this.currentTarget = null; - /* + /** * Type of the event * * @member {string} */ this.type = null; - /* + /** * InteractionData related to this event * * @member {PIXI.interaction.InteractionData} diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 610e248..e1c50d7 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -13,6 +13,14 @@ const MOUSE_POINTER_ID = 'MOUSE'; +// helpers for hitTest() - only used inside hitTest() +const hitTestEvent = { + target: null, + data: { + global: null, + }, +}; + /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true @@ -245,67 +253,60 @@ this.setTargetElement(this.renderer.view, this.renderer.resolution); /** - * Fired when a pointer device button (usually a mouse button) is pressed on the display + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * - * @event mousedown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * - * @event rightdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released over the display + * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * - * @event mouseup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * - * @event rightup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is pressed and released on + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * - * @event click - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * - * @event rightclick - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** - * Fired when a pointer device button (usually a mouse button) is released outside the + * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link PIXI.interaction.InteractionManager#event:mousedown}. * - * @event mouseupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** @@ -313,146 +314,362 @@ * outside the display object that initially registered a * [rightdown]{@link PIXI.interaction.InteractionManager#event:rightdown}. * - * @event rightupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * 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# + * @event PIXI.interaction.InteractionManager#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * - * @event mouseover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * - * @event mouseout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * - * @event pointerdown - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * - * @event pointerup - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * - * @event pointercancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * - * @event pointertap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link PIXI.interaction.InteractionManager#event:pointerdown}. * - * @event pointerupoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * - * @event pointermove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * - * @event pointerover - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * - * @event pointerout - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * - * @event touchstart - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * - * @event touchend - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * - * @event touchcancel - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * - * @event tap - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link PIXI.interaction.InteractionManager#event:touchstart}. * - * @event touchendoutside - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * - * @event touchmove - * @type {PIXI.interaction.InteractionData} - * @memberof PIXI.interaction.InteractionManager# + * @event PIXI.interaction.InteractionManager#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousedown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released over the display + * object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is pressed and released on + * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#click + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is pressed + * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightclick + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button (usually a mouse left-button) is released outside the + * display object that initially registered a + * [mousedown]{@link PIXI.DisplayObject#event:mousedown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device secondary button (usually a mouse right-button) is released + * outside the display object that initially registered a + * [rightdown]{@link PIXI.DisplayObject#event:rightdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#rightupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mousemove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device (usually a mouse) is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#mouseout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerdown + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerup + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a pointer event. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointercancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is pressed and released on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointertap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device button is released outside the display object that initially + * registered a [pointerdown]{@link PIXI.DisplayObject#event:pointerdown}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerupoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved while over the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointermove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved onto the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerover + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a pointer device is moved off the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#pointerout + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed on the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchstart + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchend + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when the operating system cancels a touch. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchcancel + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is placed and removed from the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#tap + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is removed outside of the display object that initially + * registered a [touchstart]{@link PIXI.DisplayObject#event:touchstart}. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchendoutside + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + + /** + * Fired when a touch point is moved along the display object. + * DisplayObject's `interactive` property must be set to `true` to fire event. + * + * @event PIXI.DisplayObject#touchmove + * @param {PIXI.interaction.InteractionEvent} event - Interaction event + */ + } + + /** + * Hit tests a point against the display tree, returning the first interactive object that is hit. + * + * @param {PIXI.Point} globalPoint - A point to hit test with, in global space. + * @param {PIXI.Container} [root] - The root display object to start from. If omitted, defaults + * to the last rendered root of the associated renderer. + * @return {PIXI.DisplayObject} The hit display object, if any. + */ + hitTest(globalPoint, root) + { + // clear the target for our hit test + hitTestEvent.target = null; + // assign the global point + hitTestEvent.data.global = globalPoint; + // ensure safety of the root + if (!root) + { + root = this.renderer._lastObjectRendered; + } + // run the hit test + this.processInteractive(hitTestEvent, root, null, true); + // return our found object - it'll be null if we didn't hit anything + + return hitTestEvent.target; } /** @@ -487,7 +704,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { @@ -753,7 +970,7 @@ * testing the interactive objects and passes the hit across in the function. * * @private - * @param {InteractionEvent} interactionEvent - event containing the point that + * @param {PIXI.interaction.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) @@ -881,7 +1098,10 @@ interactionEvent.target = displayObject; } - func(interactionEvent, displayObject, !!hit); + if (func) + { + func(interactionEvent, displayObject, !!hit); + } } } @@ -943,7 +1163,7 @@ * 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.interaction.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 */ @@ -1040,7 +1260,7 @@ * 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.interaction.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) @@ -1076,7 +1296,7 @@ * 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.interaction.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 */ @@ -1214,7 +1434,7 @@ * 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.interaction.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 */ @@ -1283,7 +1503,7 @@ * 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.interaction.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 */ @@ -1376,7 +1596,7 @@ * * @private * @param {PointerEvent} event - Normalized pointer event, output from normalizeToPointerData - * @return {InteractionData} - Interaction data for the given pointer identifier + * @return {PIXI.interaction.InteractionData} - Interaction data for the given pointer identifier */ getInteractionDataForPointerId(event) { @@ -1420,10 +1640,11 @@ * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PIXI.interaction.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 + * @param {PIXI.interaction.InteractionData} interactionData - The InteractionData that will be paired + * with the InteractionEvent + * @return {PIXI.interaction.InteractionEvent} the interaction event that was passed in */ configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 6a1f69e..8493b63 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -35,8 +35,10 @@ } /** + * Unique pointer id of the event + * * @readonly - * @type {number} Unique pointer id of the event + * @member {number} */ get pointerId() { diff --git a/src/interaction/index.js b/src/interaction/index.js index 56c5b09..72f7d71 100644 --- a/src/interaction/index.js +++ b/src/interaction/index.js @@ -1,4 +1,8 @@ /** + * This namespace contains a renderer plugin for handling mouse, pointer, and touch events. + * + * Do not instantiate this plugin directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. * @namespace PIXI.interaction */ export { default as InteractionData } from './InteractionData'; diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index d252fba..cebac40 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -2,7 +2,7 @@ * Default property values of interactive objects * Used by {@link PIXI.interaction.InteractionManager} to automatically give all DisplayObjects these properties * - * @mixin + * @private * @name interactiveTarget * @memberof PIXI.interaction * @example @@ -14,18 +14,28 @@ * ); */ export default { + /** - * Determines if the displayObject be clicked/touched + * Enable interaction events for the DisplayObject. Touch, pointer and mouse + * events will not be emitted unless `interactive` is set to `true`. * - * @inner {boolean} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.on('tap', (event) => { + * //handle event + * }); + * @member {boolean} + * @memberof PIXI.DisplayObject# */ interactive: false, /** * Determines if the children to the displayObject can be clicked/touched - * Setting this to false allows pixi to bypass a recursive hitTest function + * Setting this to false allows pixi to bypass a recursive `hitTest` function * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.Container# */ interactiveChildren: true, @@ -33,7 +43,12 @@ * Interaction shape. Children will be hit first, then this shape will be checked. * Setting this will cause this shape to be checked in hit tests rather than the displayObject's bounds. * - * @inner {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100); + * @member {PIXI.Rectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon|PIXI.RoundedRectangle} + * @memberof PIXI.DisplayObject# */ hitArea: null, @@ -41,8 +56,12 @@ * 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'`. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.buttonMode = true; * @member {boolean} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# */ get buttonMode() { @@ -64,9 +83,14 @@ * This defines what cursor mode is used when the mouse cursor * is hovered over the displayObject. * + * @example + * const sprite = new PIXI.Sprite(texture); + * sprite.interactive = true; + * sprite.cursor = 'wait'; * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * - * @inner {string} + * @member {string} + * @memberof PIXI.DisplayObject# */ cursor: null, @@ -74,7 +98,7 @@ * Internal set of all active pointers, by identifier * * @member {Map} - * @memberof PIXI.interaction.interactiveTarget# + * @memberof PIXI.DisplayObject# * @private */ get trackedPointers() @@ -87,7 +111,8 @@ /** * Map of all tracked pointers, by identifier. Use trackedPointers to access. * - * @private {Map} + * @private + * @type {Map} */ _trackedPointers: undefined, }; diff --git a/src/loaders/index.js b/src/loaders/index.js index 5189793..daa1fc4 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -1,9 +1,22 @@ +import Application from '../core/Application'; +import Loader from './loader'; + /** + * This namespace contains APIs which extends the {@link https://github.com/englercj/resource-loader resource-loader} module + * for loading assets, data, and other resources dynamically. + * @example + * const loader = new PIXI.loaders.Loader(); + * loader.add('bunny', 'data/bunny.png') + * .add('spaceship', 'assets/spritesheet.json'); + * loader.load((loader, resources) => { + * // resources.bunny + * // resources.spaceship + * }); * @namespace PIXI.loaders */ -export { default as Loader } from './loader'; +export { Loader }; export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; -export { default as spritesheetParser } from './spritesheetParser'; +export { default as spritesheetParser, getResourcePath } from './spritesheetParser'; export { default as textureParser } from './textureParser'; /** @@ -13,3 +26,55 @@ * @memberof PIXI.loaders */ export { Resource } from 'resource-loader'; + +/** + * A premade instance of the loader that can be used to load resources. + * @name shared + * @memberof PIXI.loaders + * @type {PIXI.loaders.Loader} + */ +const shared = new Loader(); + +shared.destroy = () => +{ + // protect destroying shared loader +}; + +export { shared }; + +// Mixin the loader construction +const AppPrototype = Application.prototype; + +AppPrototype._loader = null; + +/** + * Loader instance to help with asset loading. + * @name PIXI.Application#loader + * @type {PIXI.loaders.Loader} + */ +Object.defineProperty(AppPrototype, 'loader', { + get() + { + if (!this._loader) + { + const sharedLoader = this._options.sharedLoader; + + this._loader = sharedLoader ? shared : new Loader(); + } + + return this._loader; + }, +}); + +// Override the destroy function +// making sure to destroy the current Loader +AppPrototype._parentDestroy = AppPrototype.destroy; +AppPrototype.destroy = function destroy() +{ + if (this._loader) + { + this._loader.destroy(); + this._loader = null; + } + this._parentDestroy(); +}; diff --git a/src/loaders/loader.js b/src/loaders/loader.js index 767a411..59b8000 100644 --- a/src/loaders/loader.js +++ b/src/loaders/loader.js @@ -90,6 +90,15 @@ { Loader._pixiMiddleware.push(fn); } + + /** + * Destroy the loader, removes references. + */ + destroy() + { + this.removeAllListeners(); + this.reset(); + } } // Copy EE3 prototype (mixin) diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index e57dfb2..eda9584 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,12 +1,11 @@ import { Resource } from 'resource-loader'; -import path from 'path'; +import url from 'url'; import { Spritesheet } from '../core'; export default function () { return function spritesheetParser(resource, next) { - let resourcePath; const imageResourceName = `${resource.name}_image`; // skip if no data, its not json, it isn't spritesheet data, or the image resource already exists @@ -28,15 +27,7 @@ parentResource: resource, }; - // Prepend url path unless the resource image is a data url - if (resource.isDataUrl) - { - resourcePath = resource.data.meta.image; - } - else - { - resourcePath = `${path.dirname(resource.url.replace(this.baseUrl, ''))}/${resource.data.meta.image}`; - } + const resourcePath = getResourcePath(resource, this.baseUrl); // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) @@ -56,3 +47,14 @@ }); }; } + +export function getResourcePath(resource, baseUrl) +{ + // Prepend url path unless the resource image is a data url + if (resource.isDataUrl) + { + return resource.data.meta.image; + } + + return url.resolve(resource.url.replace(baseUrl, ''), resource.data.meta.image); +} diff --git a/src/mesh/Mesh.js b/src/mesh/Mesh.js index 02c04a0..d866f03 100644 --- a/src/mesh/Mesh.js +++ b/src/mesh/Mesh.js @@ -1,4 +1,5 @@ import * as core from '../core'; +import { default as TextureTransform } from '../extras/TextureTransform'; const tempPoint = new core.Point(); const tempPolygon = new core.Polygon(); @@ -28,7 +29,7 @@ * @member {PIXI.Texture} * @private */ - this._texture = null; + this._texture = texture; /** * The Uvs of the Mesh @@ -52,8 +53,10 @@ 100, 100, 0, 100]); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ // TODO auto generate this based on draw mode! this.indices = indices || new Uint16Array([0, 1, 3, 2]); @@ -98,9 +101,6 @@ */ this.drawMode = drawMode || Mesh.DRAW_MODES.TRIANGLE_MESH; - // run texture setter; - this.texture = texture; - /** * The default shader that is used if a mesh doesn't have a more specific one. * @@ -125,9 +125,27 @@ this._glDatas = {}; /** + * transform that is applied to UV to get the texture coords + * its updated independently from texture uvTransform + * updates of uvs are tied to that thing + * + * @member {PIXI.extras.TextureTransform} + * @private + */ + this._uvTransform = new TextureTransform(texture); + + /** + * whether or not upload uvTransform to shader + * if its false, then uvs should be pre-multiplied + * if you change it for generated mesh, please call 'refresh(true)' + * @member {boolean} + * @default false + */ + this.uploadUvTransform = false; + + /** * Plugin that is responsible for rendering this element. * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. - * * @member {string} * @default 'mesh' */ @@ -142,6 +160,7 @@ */ _renderWebGL(renderer) { + this.refresh(); renderer.setObjectRenderer(renderer.plugins[this.pluginName]); renderer.plugins[this.pluginName].render(this); } @@ -154,6 +173,7 @@ */ _renderCanvas(renderer) { + this.refresh(); renderer.plugins[this.pluginName].render(this); } @@ -164,6 +184,43 @@ */ _onTextureUpdate() { + this._uvTransform.texture = this._texture; + this.refresh(); + } + + /** + * multiplies uvs only if uploadUvTransform is false + * call it after you change uvs manually + * make sure that texture is valid + */ + multiplyUvs() + { + if (!this.uploadUvTransform) + { + this._uvTransform.multiplyUvs(this.uvs); + } + } + + /** + * Refreshes uvs for generated meshes (rope, plane) + * sometimes refreshes vertices too + * + * @param {boolean} [forceUpdate=false] if true, matrices will be updated any case + */ + refresh(forceUpdate) + { + if (this._uvTransform.update(forceUpdate)) + { + this._refresh(); + } + } + + /** + * re-calculates mesh coords + * @protected + */ + _refresh() + { /* empty */ } diff --git a/src/mesh/NineSlicePlane.js b/src/mesh/NineSlicePlane.js index 08ef76e..c243246 100644 --- a/src/mesh/NineSlicePlane.js +++ b/src/mesh/NineSlicePlane.js @@ -52,8 +52,8 @@ uvs[6] = uvs[14] = uvs[22] = uvs[30] = 1; uvs[25] = uvs[27] = uvs[29] = uvs[31] = 1; - this._origWidth = texture.width; - this._origHeight = texture.height; + this._origWidth = texture.orig.width; + this._origHeight = texture.orig.height; this._uvw = 1 / this._origWidth; this._uvh = 1 / this._origHeight; @@ -64,7 +64,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.width = texture.width; + this.width = this._origWidth; /** * The height of the NineSlicePlane, setting this will actually modify the vertices and UV's of this plane @@ -73,7 +73,7 @@ * @memberof PIXI.NineSlicePlane# * @override */ - this.height = texture.height; + this.height = this._origHeight; uvs[2] = uvs[10] = uvs[18] = uvs[26] = this._uvw * leftWidth; uvs[4] = uvs[12] = uvs[20] = uvs[28] = 1 - (this._uvw * rightWidth); @@ -115,6 +115,8 @@ * @override */ this.bottomHeight = typeof bottomHeight !== 'undefined' ? bottomHeight : DEFAULT_BORDER_SIZE; + + this.refresh(true); } /** diff --git a/src/mesh/Plane.js b/src/mesh/Plane.js index 3869a39..4f9b51c 100644 --- a/src/mesh/Plane.js +++ b/src/mesh/Plane.js @@ -43,17 +43,17 @@ } /** - * Refreshes + * Refreshes plane coordinates * */ - refresh() + _refresh() { + const texture = this._texture; const total = this.verticesX * this.verticesY; const verts = []; const colors = []; const uvs = []; const indices = []; - const texture = this.texture; const segmentsX = this.verticesX - 1; const segmentsY = this.verticesY - 1; @@ -63,24 +63,12 @@ for (let i = 0; i < total; i++) { - if (texture._uvs) - { - const x = (i % this.verticesX); - const y = ((i / this.verticesX) | 0); + const x = (i % this.verticesX); + const y = ((i / this.verticesX) | 0); - verts.push((x * sizeX), - (y * sizeY)); + verts.push(x * sizeX, y * sizeY); - // this works for rectangular textures. - uvs.push( - texture._uvs.x0 + ((texture._uvs.x1 - texture._uvs.x0) * (x / (this.verticesX - 1))), - texture._uvs.y0 + ((texture._uvs.y3 - texture._uvs.y0) * (y / (this.verticesY - 1))) - ); - } - else - { - uvs.push(0); - } + uvs.push(x / segmentsX, y / segmentsY); } // cons @@ -106,8 +94,9 @@ this.uvs = new Float32Array(uvs); this.colors = new Float32Array(colors); this.indices = new Uint16Array(indices); - this.indexDirty = true; + + this.multiplyUvs(); } /** diff --git a/src/mesh/Rope.js b/src/mesh/Rope.js index 6ff8aaf..ccee263 100644 --- a/src/mesh/Rope.js +++ b/src/mesh/Rope.js @@ -1,5 +1,4 @@ import Mesh from './Mesh'; -import * as core from '../core'; /** * The rope allows you to draw a texture across several points and them manipulate these points @@ -26,39 +25,47 @@ { super(texture); - /* - * @member {PIXI.Point[]} An array of points that determine the rope + /** + * An array of points that determine the rope + * + * @member {PIXI.Point[]} */ this.points = points; - /* - * @member {Float32Array} An array of vertices used to construct this rope. + /** + * An array of vertices used to construct this rope. + * + * @member {Float32Array} */ this.vertices = new Float32Array(points.length * 4); - /* - * @member {Float32Array} The WebGL Uvs of the rope. + /** + * The WebGL Uvs of the rope. + * + * @member {Float32Array} */ this.uvs = new Float32Array(points.length * 4); - /* - * @member {Float32Array} An array containing the color components + /** + * An array containing the color components + * + * @member {Float32Array} */ this.colors = new Float32Array(points.length * 2); - /* - * @member {Uint16Array} An array containing the indices of the vertices + /** + * An array containing the indices of the vertices + * + * @member {Uint16Array} */ this.indices = new Uint16Array(points.length * 2); /** - * Tracker for if the rope is ready to be drawn. Needed because Mesh ctor can - * call _onTextureUpdated which could call refresh too early. - * + * refreshes vertices on every updateTransform * @member {boolean} - * @private + * @default true */ - this._ready = true; + this.autoUpdate = true; this.refresh(); } @@ -67,7 +74,7 @@ * Refreshes * */ - refresh() + _refresh() { const points = this.points; @@ -91,14 +98,10 @@ const indices = this.indices; const colors = this.colors; - const textureUvs = this._texture._uvs; - const offset = new core.Point(textureUvs.x0, textureUvs.y0); - const factor = new core.Point(textureUvs.x2 - textureUvs.x0, Number(textureUvs.y2 - textureUvs.y0)); - - uvs[0] = 0 + offset.x; - uvs[1] = 0 + offset.y; - uvs[2] = 0 + offset.x; - uvs[3] = factor.y + offset.y; + uvs[0] = 0; + uvs[1] = 0; + uvs[2] = 0; + uvs[3] = 1; colors[0] = 1; colors[1] = 1; @@ -114,11 +117,11 @@ let index = i * 4; const amount = i / (total - 1); - uvs[index] = (amount * factor.x) + offset.x; - uvs[index + 1] = 0 + offset.y; + uvs[index] = amount; + uvs[index + 1] = 0; - uvs[index + 2] = (amount * factor.x) + offset.x; - uvs[index + 3] = factor.y + offset.y; + uvs[index + 2] = amount; + uvs[index + 3] = 1; index = i * 2; colors[index] = 1; @@ -132,30 +135,15 @@ // ensure that the changes are uploaded this.dirty++; this.indexDirty++; + + this.multiplyUvs(); + this.refreshVertices(); } /** - * Clear texture UVs when new texture is set - * - * @private + * refreshes vertices of Rope mesh */ - _onTextureUpdate() - { - super._onTextureUpdate(); - - // wait for the Rope ctor to finish before calling refresh - if (this._ready) - { - this.refresh(); - } - } - - /** - * Updates the object transform for rendering - * - * @private - */ - updateTransform() + refreshVertices() { const points = this.points; @@ -214,7 +202,19 @@ lastPoint = point; } + } + /** + * Updates the object transform for rendering + * + * @private + */ + updateTransform() + { + if (this.autoUpdate) + { + this.refreshVertices(); + } this.containerUpdateTransform(); } diff --git a/src/mesh/canvas/CanvasMeshRenderer.js b/src/mesh/canvas/CanvasMeshRenderer.js index 1263170..5354e2c 100644 --- a/src/mesh/canvas/CanvasMeshRenderer.js +++ b/src/mesh/canvas/CanvasMeshRenderer.js @@ -135,12 +135,33 @@ const textureWidth = base.width; const textureHeight = base.height; - const u0 = uvs[index0] * base.width; - const u1 = uvs[index1] * base.width; - const u2 = uvs[index2] * base.width; - const v0 = uvs[index0 + 1] * base.height; - const v1 = uvs[index1 + 1] * base.height; - const v2 = uvs[index2 + 1] * base.height; + let u0; + let u1; + let u2; + let v0; + let v1; + let v2; + + if (mesh.uploadUvTransform) + { + const ut = mesh._uvTransform.mapCoord; + + u0 = ((uvs[index0] * ut.a) + (uvs[index0 + 1] * ut.c) + ut.tx) * base.width; + u1 = ((uvs[index1] * ut.a) + (uvs[index1 + 1] * ut.c) + ut.tx) * base.width; + u2 = ((uvs[index2] * ut.a) + (uvs[index2 + 1] * ut.c) + ut.tx) * base.width; + v0 = ((uvs[index0] * ut.b) + (uvs[index0 + 1] * ut.d) + ut.ty) * base.height; + v1 = ((uvs[index1] * ut.b) + (uvs[index1 + 1] * ut.d) + ut.ty) * base.height; + v2 = ((uvs[index2] * ut.b) + (uvs[index2 + 1] * ut.d) + ut.ty) * base.height; + } + else + { + u0 = uvs[index0] * base.width; + u1 = uvs[index1] * base.width; + u2 = uvs[index2] * base.width; + v0 = uvs[index0 + 1] * base.height; + v1 = uvs[index1 + 1] * base.height; + v2 = uvs[index2 + 1] * base.height; + } let x0 = vertices[index0]; let x1 = vertices[index1]; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index 8e61919..9d59adb 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -4,6 +4,8 @@ import { readFileSync } from 'fs'; import { join } from 'path'; +const matrixIdentity = core.Matrix.IDENTITY; + /** * WebGL renderer plugin for tiling sprites * @@ -104,6 +106,17 @@ renderer.state.setBlendMode(mesh.blendMode); + if (glData.shader.uniforms.uTransform) + { + if (mesh.uploadUvTransform) + { + glData.shader.uniforms.uTransform = mesh._uvTransform.mapCoord.toArray(true); + } + else + { + glData.shader.uniforms.uTransform = matrixIdentity.toArray(true); + } + } glData.shader.uniforms.translationMatrix = mesh.worldTransform.toArray(true); glData.shader.uniforms.alpha = mesh.worldAlpha; glData.shader.uniforms.tint = mesh.tintRgb; diff --git a/src/mesh/webgl/mesh.vert b/src/mesh/webgl/mesh.vert index a337aef..acc096c 100644 --- a/src/mesh/webgl/mesh.vert +++ b/src/mesh/webgl/mesh.vert @@ -1,8 +1,9 @@ attribute vec2 aVertexPosition; attribute vec2 aTextureCoord; -uniform mat3 translationMatrix; uniform mat3 projectionMatrix; +uniform mat3 translationMatrix; +uniform mat3 uTransform; varying vec2 vTextureCoord; @@ -10,5 +11,5 @@ { gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); - vTextureCoord = aTextureCoord; + vTextureCoord = (uTransform * vec3(aTextureCoord, 1.0)).xy; } diff --git a/src/particles/ParticleContainer.js b/src/particles/ParticleContainer.js index 67796cf..63f7791 100644 --- a/src/particles/ParticleContainer.js +++ b/src/particles/ParticleContainer.js @@ -313,10 +313,10 @@ frame.y * resolution, frame.width * resolution, frame.height * resolution, - positionX * resolution, - positionY * resolution, - finalWidth * resolution, - finalHeight * resolution + positionX * renderer.resolution, + positionY * renderer.resolution, + finalWidth * renderer.resolution, + finalHeight * renderer.resolution ); } } diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index a4aae2f..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -18,6 +18,18 @@ * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} * to provide preparation capabilities specific to their respective renderers. * + * @example + * // Create a sprite + * const sprite = new PIXI.Sprite.fromImage('something.png'); + * + * // Load object into GPU + * app.renderer.plugins.prepare.upload(sprite, () => { + * + * //Texture(s) has been uploaded to GPU + * app.stage.addChild(sprite); + * + * }) + * * @abstract * @class * @memberof PIXI.prepare @@ -100,8 +112,16 @@ this.prepareItems(); }; - this.register(findText, drawText); - this.register(findTextStyle, calculateTextStyle); + // hooks to find the correct texture + this.registerFindHook(findText); + this.registerFindHook(findTextStyle); + this.registerFindHook(findMultipleBaseTextures); + this.registerFindHook(findBaseTexture); + this.registerFindHook(findTexture); + + // upload hooks + this.registerUploadHook(drawText); + this.registerUploadHook(calculateTextStyle); } /** @@ -138,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -208,26 +228,36 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } /** - * Adds hooks for finding and uploading items. + * Adds hooks for finding items. * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + * @param {Function} addHook - Function call that takes two parameters: `item:*, queue:Array` + * function must return `true` if it was able to add item to the queue. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. */ - register(addHook, uploadHook) + registerFindHook(addHook) { if (addHook) { this.addHooks.push(addHook); } + return this; + } + + /** + * Adds hooks for uploading items. + * + * @param {Function} uploadHook - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.BasePrepare} Instance of plugin for chaining. + */ + registerUploadHook(uploadHook) + { if (uploadHook) { this.uploadHooks.push(uploadHook); @@ -290,6 +320,88 @@ } /** + * Built-in hook to find multiple textures from objects like AnimatedSprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findMultipleBaseTextures(item, queue) +{ + let result = false; + + // Objects with mutliple textures + if (item && item._textures && item._textures.length) + { + for (let i = 0; i < item._textures.length; i++) + { + if (item._textures[i] instanceof core.Texture) + { + const baseTexture = item._textures[i].baseTexture; + + if (queue.indexOf(baseTexture) === -1) + { + queue.push(baseTexture); + result = true; + } + } + } + } + + return result; +} + +/** + * Built-in hook to find BaseTextures from Sprites. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findBaseTexture(item, queue) +{ + // Objects with textures, like Sprites/Text + if (item instanceof core.BaseTexture) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find textures from objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Texture object was found. + */ +function findTexture(item, queue) +{ + if (item._texture && item._texture instanceof core.Texture) + { + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** * Built-in hook to draw PIXI.Text to its texture. * * @private diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 0224bb5..09c207a 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -43,7 +43,7 @@ this.ctx = this.canvas.getContext('2d'); // Add textures to upload - this.register(findBaseTextures, uploadBaseTextures); + this.registerUploadHook(uploadBaseTextures); } /** @@ -89,39 +89,4 @@ return false; } -/** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item -Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - core.CanvasRenderer.registerPlugin('prepare', CanvasPrepare); diff --git a/src/prepare/index.js b/src/prepare/index.js index f559c45..e7491ec 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -1,4 +1,29 @@ /** + * The prepare namespace provides renderer-specific plugins for pre-rendering DisplayObjects. These plugins are useful for + * asynchronously preparing assets, textures, graphics waiting to be displayed. + * + * Do not instantiate these plugins directly. It is available from the `renderer.plugins` property. + * See {@link PIXI.CanvasRenderer#plugins} or {@link PIXI.WebGLRenderer#plugins}. + * @example + * // Create a new application + * const app = new PIXI.Application(); + * document.body.appendChild(app.view); + * + * // Don't start rendering right away + * app.stop(); + * + * // create a display object + * const rect = new PIXI.Graphics() + * .beginFill(0x00ff00) + * .drawRect(40, 40, 200, 200); + * + * // Add to the stage + * app.stage.addChild(rect); + * + * // Don't start rendering until the graphic is uploaded to the GPU + * app.renderer.plugins.prepare.upload(app.stage, () => { + * app.start(); + * }); * @namespace PIXI.prepare */ export { default as webgl } from './webgl/WebGLPrepare'; diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index eb7023f..9df3c3f 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -22,12 +22,11 @@ this.uploadHookHelper = this.renderer; // Add textures and graphics to upload - this.register(findBaseTextures, uploadBaseTextures) - .register(findGraphics, uploadGraphics); + this.registerFindHook(findGraphics); + this.registerUploadHook(uploadBaseTextures); + this.registerUploadHook(uploadGraphics); } - } - /** * Built-in hook to upload PIXI.Texture objects to the GPU. * @@ -80,41 +79,6 @@ } /** - * Built-in hook to find textures from Sprites. - * - * @private - * @param {PIXI.DisplayObject} item - Display object to check - * @param {Array<*>} queue - Collection of items to upload - * @return {boolean} if a PIXI.Texture object was found. - */ -function findBaseTextures(item, queue) -{ - // Objects with textures, like Sprites/Text - if (item instanceof core.BaseTexture) - { - if (queue.indexOf(item) === -1) - { - queue.push(item); - } - - return true; - } - else if (item._texture && item._texture instanceof core.Texture) - { - const texture = item._texture.baseTexture; - - if (queue.indexOf(texture) === -1) - { - queue.push(texture); - } - - return true; - } - - return false; -} - -/** * Built-in hook to find graphics. * * @private diff --git a/test/core/BaseTexture.js b/test/core/BaseTexture.js index 8b93478..ac44c6a 100644 --- a/test/core/BaseTexture.js +++ b/test/core/BaseTexture.js @@ -1,11 +1,28 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('BaseTexture', function () { describe('updateImageType', function () { it('should allow no extension', function () { + cleanCache(); + const baseTexture = new PIXI.BaseTexture(); baseTexture.imageUrl = 'http://some.domain.org/100/100'; @@ -14,4 +31,74 @@ expect(baseTexture.imageType).to.be.equals('png'); }); }); + + it('should remove Canvas BaseTexture from cache on destroy', function () + { + cleanCache(); + + const canvas = document.createElement('canvas'); + const baseTexture = PIXI.BaseTexture.fromCanvas(canvas); + const _pixiId = canvas._pixiId; + + expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(baseTexture); + baseTexture.destroy(); + expect(baseTexture.textureCacheIds).to.equal(null); + expect(PIXI.utils.BaseTextureCache[_pixiId]).to.equal(undefined); + }); + + it('should remove Image BaseTexture from cache on destroy', function () + { + cleanCache(); + + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.baseTexture.textureCacheIds.indexOf(URL)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + texture.destroy(true); + expect(texture.baseTexture).to.equal(null); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(undefined); + }); + + it('should remove BaseTexture from entire cache using removeFromCache (by BaseTexture instance)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(baseTexture); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove BaseTexture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const baseTexture = new PIXI.BaseTexture(); + + PIXI.BaseTexture.addToCache(baseTexture, NAME); + PIXI.BaseTexture.addToCache(baseTexture, NAME2); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(baseTexture); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + PIXI.BaseTexture.removeFromCache(NAME); + expect(baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(baseTexture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.BaseTextureCache[NAME2]).to.equal(baseTexture); + }); }); diff --git a/test/core/Bounds.js b/test/core/Bounds.js index d2893a6..454a47c 100644 --- a/test/core/Bounds.js +++ b/test/core/Bounds.js @@ -350,7 +350,7 @@ expect(bounds.height).to.equal(20); }); - it('should register correct width/height with an a Text Object', function () + it('should register correct width/height with a Text Object', function () { const parent = new PIXI.Container(); diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js index 283adf5..7a3d6ac 100644 --- a/test/core/Spritesheet.js +++ b/test/core/Spritesheet.js @@ -18,6 +18,7 @@ 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); + expect(textures[id].textureCacheIds.indexOf(id)).to.equal(0); spritesheet.destroy(true); expect(spritesheet.textures).to.be.null; expect(spritesheet.baseTexture).to.be.null; diff --git a/test/core/Text.js b/test/core/Text.js index 5799ad4..e678ba4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -2,40 +2,6 @@ describe('PIXI.Text', function () { - describe('getFontStyle', function () - { - it('should be a valid API', function () - { - expect(PIXI.Text.getFontStyle).to.be.a.function; - }); - - it('should assume pixel fonts', function () - { - const style = PIXI.Text.getFontStyle({ fontSize: 72 }); - - expect(style).to.be.a.string; - expect(style).to.have.string(' 72px '); - }); - - it('should handle multiple fonts as array', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: ['Georgia', 'Arial', 'sans-serif'], - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - - it('should handle multiple fonts as string', function () - { - const style = PIXI.Text.getFontStyle({ - fontFamily: 'Georgia, "Arial", sans-serif', - }); - - expect(style).to.have.string('"Georgia","Arial","sans-serif"'); - }); - }); - describe('destroy', function () { it('should call through to Sprite.destroy', function () diff --git a/test/core/TextStyle.js b/test/core/TextStyle.js index b5761ad..49b99ef 100644 --- a/test/core/TextStyle.js +++ b/test/core/TextStyle.js @@ -23,4 +23,31 @@ expect(textStyle.fontSize).to.equal(1000); expect(clonedTextStyle.fontSize).to.equal(textStyle.fontSize); }); + + it('should assume pixel fonts', function () + { + const style = new PIXI.TextStyle({ fontSize: 72 }); + const font = style.toFontString(); + + expect(font).to.be.a.string; + expect(font).to.have.string(' 72px '); + }); + + it('should handle multiple fonts as array', function () + { + const style = new PIXI.TextStyle({ + fontFamily: ['Georgia', 'Arial', 'sans-serif'], + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); + + it('should handle multiple fonts as string', function () + { + const style = new PIXI.TextStyle({ + fontFamily: 'Georgia, "Arial", sans-serif', + }); + + expect(style.toFontString()).to.have.string('"Georgia","Arial","sans-serif"'); + }); }); diff --git a/test/core/Texture.js b/test/core/Texture.js index 8c4ef98..995ed40 100644 --- a/test/core/Texture.js +++ b/test/core/Texture.js @@ -1,11 +1,26 @@ 'use strict'; +const URL = 'foo.png'; +const NAME = 'foo'; +const NAME2 = 'bar'; + +function cleanCache() +{ + delete PIXI.utils.BaseTextureCache[URL]; + delete PIXI.utils.BaseTextureCache[NAME]; + delete PIXI.utils.BaseTextureCache[NAME2]; + + delete PIXI.utils.TextureCache[URL]; + delete PIXI.utils.TextureCache[NAME]; + delete PIXI.utils.TextureCache[NAME2]; +} + describe('PIXI.Texture', function () { it('should register Texture from Loader', function () { - const URL = 'foo.png'; - const NAME = 'bar'; + cleanCache(); + const image = new Image(); const texture = PIXI.Texture.fromLoader(image, URL, NAME); @@ -16,4 +31,100 @@ expect(PIXI.utils.TextureCache[URL]).to.equal(texture); expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); }); + + it('should remove Texture from cache on destroy', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + texture.destroy(); + expect(texture.textureCacheIds).to.equal(null); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly, ' + + 'and should remove only itself, not effecting the base texture and its cache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should be added to the texture cache correctly using legacy addTextureToCache, ' + + 'and should remove also remove the base texture from its cache with removeTextureFromCache', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.BaseTexture.addToCache(texture.baseTexture, NAME); + PIXI.Texture.addTextureToCache(texture, NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + PIXI.Texture.removeTextureFromCache(NAME); + expect(texture.baseTexture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + }); + + it('should remove Texture from entire cache using removeFromCache (by Texture instance)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(texture); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(-1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(undefined); + }); + + it('should remove Texture from single cache entry using removeFromCache (by id)', function () + { + cleanCache(); + + const texture = new PIXI.Texture(new PIXI.BaseTexture()); + + PIXI.Texture.addToCache(texture, NAME); + PIXI.Texture.addToCache(texture, NAME2); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(0); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(1); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + PIXI.Texture.removeFromCache(NAME); + expect(texture.textureCacheIds.indexOf(NAME)).to.equal(-1); + expect(texture.textureCacheIds.indexOf(NAME2)).to.equal(0); + expect(PIXI.utils.TextureCache[NAME]).to.equal(undefined); + expect(PIXI.utils.TextureCache[NAME2]).to.equal(texture); + }); }); diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..188770f --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,322 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should destroy on listener', function (done) + { + const ticker = new Ticker(); + const listener2 = sinon.spy(); + const listener = sinon.spy(() => + { + ticker.destroy(); + setTimeout(() => + { + expect(listener2.called).to.be.false; + expect(listener.calledOnce).to.be.true; + done(); + }, 0); + }); + + ticker.add(listener); + ticker.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + ticker.start(); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 8530131..28636ab 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -17,6 +17,7 @@ require('./util'); require('./Plane'); require('./Point'); +require('./Polygon'); require('./ObservablePoint'); require('./Matrix'); require('./Rectangle'); @@ -26,5 +27,7 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./BaseTexture'); require('./Texture'); +require('./Ticker'); require('./filters'); diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index c87fa5c..31385b9 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -1198,4 +1198,85 @@ expect(pointer.interaction.activeInteractionData[42]).to.be.undefined; }); }); + + describe('hitTest()', function () + { + it('should return hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10)); + + expect(hit).to.equal(graphics); + }); + + it('should return null if not hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(60, 60)); + + expect(hit).to.be.null; + }); + + it('should return top thing that was hit', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const behind = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(behind); + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + behind.beginFill(0xFFFFFF); + behind.drawRect(0, 0, 50, 50); + behind.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10)); + + expect(hit).to.equal(graphics); + }); + + it('should return hit when passing in root', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const behind = new PIXI.Graphics(); + const pointer = this.pointer = new MockPointer(stage); + + stage.addChild(behind); + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + behind.beginFill(0xFFFFFF); + behind.drawRect(0, 0, 50, 50); + behind.interactive = true; + + pointer.render(); + const hit = pointer.interaction.hitTest(new PIXI.Point(10, 10), behind); + + expect(hit).to.equal(behind); + }); + }); }); diff --git a/test/loaders/spritesheetParser.js b/test/loaders/spritesheetParser.js index 79f8c15..4d54805 100644 --- a/test/loaders/spritesheetParser.js +++ b/test/loaders/spritesheetParser.js @@ -60,11 +60,46 @@ .that.is.an.instanceof(PIXI.Texture); }); + it('should build the image url', function () + { + function getResourcePath(url, image) + { + return PIXI.loaders.getResourcePath({ + url, + data: { meta: { image } }, + }); + } + + let result = getResourcePath('http://some.com/spritesheet.json', 'img.png'); + + expect(result).to.be.equals('http://some.com/img.png'); + + result = getResourcePath('http://some.com/some/dir/spritesheet.json', 'img.png'); + expect(result).to.be.equals('http://some.com/some/dir/img.png'); + + result = getResourcePath('http://some.com/some/dir/spritesheet.json', './img.png'); + expect(result).to.be.equals('http://some.com/some/dir/img.png'); + + result = getResourcePath('http://some.com/some/dir/spritesheet.json', '../img.png'); + expect(result).to.be.equals('http://some.com/some/img.png'); + + result = getResourcePath('/spritesheet.json', 'img.png'); + expect(result).to.be.equals('/img.png'); + + result = getResourcePath('/some/dir/spritesheet.json', 'img.png'); + expect(result).to.be.equals('/some/dir/img.png'); + + result = getResourcePath('/some/dir/spritesheet.json', './img.png'); + expect(result).to.be.equals('/some/dir/img.png'); + + result = getResourcePath('/some/dir/spritesheet.json', '../img.png'); + expect(result).to.be.equals('/some/img.png'); + }); + // TODO: Test that rectangles are created correctly. // TODO: Test that bathc processing works correctly. // TODO: Test that resolution processing works correctly. // TODO: Test that metadata is honored. - // TODO: Test data-url code paths. }); function createMockResource(type, data) diff --git a/test/prepare/BasePrepare.js b/test/prepare/BasePrepare.js index 3cfa476..26dc8ad 100644 --- a/test/prepare/BasePrepare.js +++ b/test/prepare/BasePrepare.js @@ -10,7 +10,7 @@ expect(prep.renderer).to.equal(renderer); expect(prep.uploadHookHelper).to.be.null; expect(prep.queue).to.be.empty; - expect(prep.addHooks).to.have.lengthOf(2); + expect(prep.addHooks).to.have.lengthOf(5); expect(prep.uploadHooks).to.have.lengthOf(2); expect(prep.completes).to.be.empty; @@ -23,10 +23,11 @@ function uploadHook() { /* empty */ } const prep = new PIXI.prepare.BasePrepare(); - prep.register(addHook, uploadHook); + prep.registerFindHook(addHook); + prep.registerUploadHook(uploadHook); expect(prep.addHooks).to.contain(addHook); - expect(prep.addHooks).to.have.lengthOf(3); + expect(prep.addHooks).to.have.lengthOf(6); expect(prep.uploadHooks).to.contain(uploadHook); expect(prep.uploadHooks).to.have.lengthOf(3); @@ -58,7 +59,8 @@ }); const complete = sinon.spy(function () { /* empty */ }); - prep.register(addHook, uploadHook); + prep.registerFindHook(addHook); + prep.registerUploadHook(uploadHook); prep.upload(uploadItem, complete); expect(prep.queue).to.contain(uploadItem); @@ -82,7 +84,7 @@ } const complete = sinon.spy(function () { /* empty */ }); - prep.register(addHook); + prep.registerFindHook(addHook); prep.upload({}, complete); expect(complete.calledOnce).to.be.true; @@ -106,7 +108,8 @@ }); const complete = sinon.spy(function () { /* empty */ }); - prep.register(addHook, uploadHook); + prep.registerFindHook(addHook); + prep.registerUploadHook(uploadHook); prep.upload({}, complete); expect(prep.queue).to.have.lengthOf(1); @@ -137,7 +140,8 @@ }); const complete = sinon.spy(function () { /* empty */ }); - prep.register(addHook, uploadHook); + prep.registerFindHook(addHook); + prep.registerUploadHook(uploadHook); const item = {}; prep.upload(item, complete); @@ -181,7 +185,8 @@ done(); } - prep.register(addHook, uploadHook); + prep.registerFindHook(addHook); + prep.registerUploadHook(uploadHook); prep.upload({}, complete); expect(prep.queue).to.have.lengthOf(1);