diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/packages/canvas/canvas-sprite-tiling/package.json b/packages/canvas/canvas-sprite-tiling/package.json index 27ec81a..f572074 100644 --- a/packages/canvas/canvas-sprite-tiling/package.json +++ b/packages/canvas/canvas-sprite-tiling/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/canvas-sprite": "^5.0.0-rc", "@pixi/sprite-tiling": "^5.0.0-rc", "@pixi/utils": "^5.0.0-rc" diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/packages/canvas/canvas-sprite-tiling/package.json b/packages/canvas/canvas-sprite-tiling/package.json index 27ec81a..f572074 100644 --- a/packages/canvas/canvas-sprite-tiling/package.json +++ b/packages/canvas/canvas-sprite-tiling/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/canvas-sprite": "^5.0.0-rc", "@pixi/sprite-tiling": "^5.0.0-rc", "@pixi/utils": "^5.0.0-rc" diff --git a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js index 8db6ed9..8f4e0e4 100644 --- a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js +++ b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js @@ -1,5 +1,5 @@ import { TilingSprite } from '@pixi/sprite-tiling'; -import { CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasRenderTarget } from '@pixi/utils'; /** @@ -40,8 +40,8 @@ // Tint the tiling sprite if (this.tint !== 0xFFFFFF) { - this.tintedTexture = CanvasTinter.getTintedTexture(this, this.tint); - tempCanvas.context.drawImage(this.tintedTexture, 0, 0); + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + tempCanvas.context.drawImage(this._tintedCanvas, 0, 0); } else { diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/packages/canvas/canvas-sprite-tiling/package.json b/packages/canvas/canvas-sprite-tiling/package.json index 27ec81a..f572074 100644 --- a/packages/canvas/canvas-sprite-tiling/package.json +++ b/packages/canvas/canvas-sprite-tiling/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/canvas-sprite": "^5.0.0-rc", "@pixi/sprite-tiling": "^5.0.0-rc", "@pixi/utils": "^5.0.0-rc" diff --git a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js index 8db6ed9..8f4e0e4 100644 --- a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js +++ b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js @@ -1,5 +1,5 @@ import { TilingSprite } from '@pixi/sprite-tiling'; -import { CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasRenderTarget } from '@pixi/utils'; /** @@ -40,8 +40,8 @@ // Tint the tiling sprite if (this.tint !== 0xFFFFFF) { - this.tintedTexture = CanvasTinter.getTintedTexture(this, this.tint); - tempCanvas.context.drawImage(this.tintedTexture, 0, 0); + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + tempCanvas.context.drawImage(this._tintedCanvas, 0, 0); } else { diff --git a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js index ebf7397..d95fde5 100644 --- a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js +++ b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js @@ -1,6 +1,6 @@ import { SCALE_MODES, BLEND_MODES } from '@pixi/constants'; import { Matrix, GroupD8 } from '@pixi/math'; -import CanvasTinter from './CanvasTinter'; +import { CanvasTinter } from '@pixi/canvas-renderer'; const canvasRenderWorldTransform = new Matrix(); @@ -149,12 +149,12 @@ if (sprite.tint !== 0xFFFFFF) { - if (sprite.cachedTint !== sprite.tint || sprite.tintedTexture.tintId !== sprite._texture._updateID) + if (sprite._cachedTint !== sprite.tint || sprite._tintedCanvas.tintId !== sprite._texture._updateID) { - sprite.cachedTint = sprite.tint; + sprite._cachedTint = sprite.tint; // TODO clean up caching - how to clean up the caches? - sprite.tintedTexture = CanvasTinter.getTintedTexture(sprite, sprite.tint); + sprite._tintedCanvas = CanvasTinter.getTintedCanvas(sprite, sprite.tint); } context.drawImage( diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/packages/canvas/canvas-sprite-tiling/package.json b/packages/canvas/canvas-sprite-tiling/package.json index 27ec81a..f572074 100644 --- a/packages/canvas/canvas-sprite-tiling/package.json +++ b/packages/canvas/canvas-sprite-tiling/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/canvas-sprite": "^5.0.0-rc", "@pixi/sprite-tiling": "^5.0.0-rc", "@pixi/utils": "^5.0.0-rc" diff --git a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js index 8db6ed9..8f4e0e4 100644 --- a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js +++ b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js @@ -1,5 +1,5 @@ import { TilingSprite } from '@pixi/sprite-tiling'; -import { CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasRenderTarget } from '@pixi/utils'; /** @@ -40,8 +40,8 @@ // Tint the tiling sprite if (this.tint !== 0xFFFFFF) { - this.tintedTexture = CanvasTinter.getTintedTexture(this, this.tint); - tempCanvas.context.drawImage(this.tintedTexture, 0, 0); + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + tempCanvas.context.drawImage(this._tintedCanvas, 0, 0); } else { diff --git a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js index ebf7397..d95fde5 100644 --- a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js +++ b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js @@ -1,6 +1,6 @@ import { SCALE_MODES, BLEND_MODES } from '@pixi/constants'; import { Matrix, GroupD8 } from '@pixi/math'; -import CanvasTinter from './CanvasTinter'; +import { CanvasTinter } from '@pixi/canvas-renderer'; const canvasRenderWorldTransform = new Matrix(); @@ -149,12 +149,12 @@ if (sprite.tint !== 0xFFFFFF) { - if (sprite.cachedTint !== sprite.tint || sprite.tintedTexture.tintId !== sprite._texture._updateID) + if (sprite._cachedTint !== sprite.tint || sprite._tintedCanvas.tintId !== sprite._texture._updateID) { - sprite.cachedTint = sprite.tint; + sprite._cachedTint = sprite.tint; // TODO clean up caching - how to clean up the caches? - sprite.tintedTexture = CanvasTinter.getTintedTexture(sprite, sprite.tint); + sprite._tintedCanvas = CanvasTinter.getTintedCanvas(sprite, sprite.tint); } context.drawImage( diff --git a/packages/canvas/canvas-sprite/src/CanvasTinter.js b/packages/canvas/canvas-sprite/src/CanvasTinter.js deleted file mode 100644 index 98c72e9..0000000 --- a/packages/canvas/canvas-sprite/src/CanvasTinter.js +++ /dev/null @@ -1,290 +0,0 @@ -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import { canUseNewCanvasBlendModes } from '@pixi/canvas-renderer'; - -/** - * Utility methods for Sprite/Texture tinting. - * - * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, - * so be aware of the performance implications. - * - * @class - * @memberof PIXI - */ -const CanvasTinter = { - /** - * Basically this method just needs a sprite and a color and tints the sprite with the given color. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Sprite} sprite - the sprite to tint - * @param {number} color - the color to use to tint the sprite with - * @return {HTMLCanvasElement} The tinted canvas - */ - getTintedTexture: (sprite, color) => - { - const texture = sprite._texture; - - color = CanvasTinter.roundColor(color); - - const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - - texture.tintCache = texture.tintCache || {}; - - const cachedTexture = texture.tintCache[stringColor]; - - let canvas; - - if (cachedTexture) - { - if (cachedTexture.tintId === texture._updateID) - { - return texture.tintCache[stringColor]; - } - - canvas = texture.tintCache[stringColor]; - } - else - { - canvas = CanvasTinter.canvas || document.createElement('canvas'); - } - - CanvasTinter.tintMethod(texture, color, canvas); - - canvas.tintId = texture._updateID; - - if (CanvasTinter.convertTintToImage) - { - // is this better? - const tintImage = new Image(); - - tintImage.src = canvas.toDataURL(); - - texture.tintCache[stringColor] = tintImage; - } - else - { - texture.tintCache[stringColor] = canvas; - // if we are not converting the texture to an image then we need to lose the reference to the canvas - CanvasTinter.canvas = null; - } - - return canvas; - }, - - /** - * Tint a texture using the 'multiply' operation. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithMultiply: (texture, color, canvas) => - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - - context.fillRect(0, 0, crop.width, crop.height); - - context.globalCompositeOperation = 'multiply'; - - const source = texture.baseTexture.getDrawableSource(); - - context.drawImage( - source, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - - context.globalCompositeOperation = 'destination-atop'; - - context.drawImage( - source, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - context.restore(); - }, - - /** - * Tint a texture using the 'overlay' operation. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithOverlay(texture, color, canvas) - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.globalCompositeOperation = 'copy'; - context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - context.fillRect(0, 0, crop.width, crop.height); - - context.globalCompositeOperation = 'destination-atop'; - context.drawImage( - texture.baseTexture.getDrawableSource(), - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - - // context.globalCompositeOperation = 'copy'; - context.restore(); - }, - - /** - * Tint a texture pixel per pixel. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithPerPixel: (texture, color, canvas) => - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.globalCompositeOperation = 'copy'; - context.drawImage( - texture.baseTexture.getDrawableSource(), - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - context.restore(); - - const rgbValues = hex2rgb(color); - const r = rgbValues[0]; - const g = rgbValues[1]; - const b = rgbValues[2]; - - const pixelData = context.getImageData(0, 0, crop.width, crop.height); - - const pixels = pixelData.data; - - for (let i = 0; i < pixels.length; i += 4) - { - pixels[i + 0] *= r; - pixels[i + 1] *= g; - pixels[i + 2] *= b; - } - - context.putImageData(pixelData, 0, 0); - }, - - /** - * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. - * - * @memberof PIXI.CanvasTinter - * @param {number} color - the color to round, should be a hex color - * @return {number} The rounded color. - */ - roundColor: (color) => - { - const step = CanvasTinter.cacheStepsPerColorChannel; - - const rgbValues = hex2rgb(color); - - rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); - rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); - rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); - - return rgb2hex(rgbValues); - }, - - /** - * Number of steps which will be used as a cap when rounding colors. - * - * @memberof PIXI.CanvasTinter - * @type {number} - */ - cacheStepsPerColorChannel: 8, - - /** - * Tint cache boolean flag. - * - * @memberof PIXI.CanvasTinter - * @type {boolean} - */ - convertTintToImage: false, - - /** - * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. - * - * @memberof PIXI.CanvasTinter - * @type {boolean} - */ - canUseMultiply: canUseNewCanvasBlendModes(), - - /** - * The tinting method that will be used. - * - * @memberof PIXI.CanvasTinter - * @type {Function} - */ - tintMethod: () => - { // jslint-disable no-empty-function - - }, -}; - -CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; - -export default CanvasTinter; diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/packages/canvas/canvas-sprite-tiling/package.json b/packages/canvas/canvas-sprite-tiling/package.json index 27ec81a..f572074 100644 --- a/packages/canvas/canvas-sprite-tiling/package.json +++ b/packages/canvas/canvas-sprite-tiling/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/canvas-sprite": "^5.0.0-rc", "@pixi/sprite-tiling": "^5.0.0-rc", "@pixi/utils": "^5.0.0-rc" diff --git a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js index 8db6ed9..8f4e0e4 100644 --- a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js +++ b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js @@ -1,5 +1,5 @@ import { TilingSprite } from '@pixi/sprite-tiling'; -import { CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasRenderTarget } from '@pixi/utils'; /** @@ -40,8 +40,8 @@ // Tint the tiling sprite if (this.tint !== 0xFFFFFF) { - this.tintedTexture = CanvasTinter.getTintedTexture(this, this.tint); - tempCanvas.context.drawImage(this.tintedTexture, 0, 0); + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + tempCanvas.context.drawImage(this._tintedCanvas, 0, 0); } else { diff --git a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js index ebf7397..d95fde5 100644 --- a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js +++ b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js @@ -1,6 +1,6 @@ import { SCALE_MODES, BLEND_MODES } from '@pixi/constants'; import { Matrix, GroupD8 } from '@pixi/math'; -import CanvasTinter from './CanvasTinter'; +import { CanvasTinter } from '@pixi/canvas-renderer'; const canvasRenderWorldTransform = new Matrix(); @@ -149,12 +149,12 @@ if (sprite.tint !== 0xFFFFFF) { - if (sprite.cachedTint !== sprite.tint || sprite.tintedTexture.tintId !== sprite._texture._updateID) + if (sprite._cachedTint !== sprite.tint || sprite._tintedCanvas.tintId !== sprite._texture._updateID) { - sprite.cachedTint = sprite.tint; + sprite._cachedTint = sprite.tint; // TODO clean up caching - how to clean up the caches? - sprite.tintedTexture = CanvasTinter.getTintedTexture(sprite, sprite.tint); + sprite._tintedCanvas = CanvasTinter.getTintedCanvas(sprite, sprite.tint); } context.drawImage( diff --git a/packages/canvas/canvas-sprite/src/CanvasTinter.js b/packages/canvas/canvas-sprite/src/CanvasTinter.js deleted file mode 100644 index 98c72e9..0000000 --- a/packages/canvas/canvas-sprite/src/CanvasTinter.js +++ /dev/null @@ -1,290 +0,0 @@ -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import { canUseNewCanvasBlendModes } from '@pixi/canvas-renderer'; - -/** - * Utility methods for Sprite/Texture tinting. - * - * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, - * so be aware of the performance implications. - * - * @class - * @memberof PIXI - */ -const CanvasTinter = { - /** - * Basically this method just needs a sprite and a color and tints the sprite with the given color. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Sprite} sprite - the sprite to tint - * @param {number} color - the color to use to tint the sprite with - * @return {HTMLCanvasElement} The tinted canvas - */ - getTintedTexture: (sprite, color) => - { - const texture = sprite._texture; - - color = CanvasTinter.roundColor(color); - - const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - - texture.tintCache = texture.tintCache || {}; - - const cachedTexture = texture.tintCache[stringColor]; - - let canvas; - - if (cachedTexture) - { - if (cachedTexture.tintId === texture._updateID) - { - return texture.tintCache[stringColor]; - } - - canvas = texture.tintCache[stringColor]; - } - else - { - canvas = CanvasTinter.canvas || document.createElement('canvas'); - } - - CanvasTinter.tintMethod(texture, color, canvas); - - canvas.tintId = texture._updateID; - - if (CanvasTinter.convertTintToImage) - { - // is this better? - const tintImage = new Image(); - - tintImage.src = canvas.toDataURL(); - - texture.tintCache[stringColor] = tintImage; - } - else - { - texture.tintCache[stringColor] = canvas; - // if we are not converting the texture to an image then we need to lose the reference to the canvas - CanvasTinter.canvas = null; - } - - return canvas; - }, - - /** - * Tint a texture using the 'multiply' operation. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithMultiply: (texture, color, canvas) => - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - - context.fillRect(0, 0, crop.width, crop.height); - - context.globalCompositeOperation = 'multiply'; - - const source = texture.baseTexture.getDrawableSource(); - - context.drawImage( - source, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - - context.globalCompositeOperation = 'destination-atop'; - - context.drawImage( - source, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - context.restore(); - }, - - /** - * Tint a texture using the 'overlay' operation. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithOverlay(texture, color, canvas) - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.globalCompositeOperation = 'copy'; - context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - context.fillRect(0, 0, crop.width, crop.height); - - context.globalCompositeOperation = 'destination-atop'; - context.drawImage( - texture.baseTexture.getDrawableSource(), - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - - // context.globalCompositeOperation = 'copy'; - context.restore(); - }, - - /** - * Tint a texture pixel per pixel. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithPerPixel: (texture, color, canvas) => - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.globalCompositeOperation = 'copy'; - context.drawImage( - texture.baseTexture.getDrawableSource(), - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - context.restore(); - - const rgbValues = hex2rgb(color); - const r = rgbValues[0]; - const g = rgbValues[1]; - const b = rgbValues[2]; - - const pixelData = context.getImageData(0, 0, crop.width, crop.height); - - const pixels = pixelData.data; - - for (let i = 0; i < pixels.length; i += 4) - { - pixels[i + 0] *= r; - pixels[i + 1] *= g; - pixels[i + 2] *= b; - } - - context.putImageData(pixelData, 0, 0); - }, - - /** - * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. - * - * @memberof PIXI.CanvasTinter - * @param {number} color - the color to round, should be a hex color - * @return {number} The rounded color. - */ - roundColor: (color) => - { - const step = CanvasTinter.cacheStepsPerColorChannel; - - const rgbValues = hex2rgb(color); - - rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); - rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); - rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); - - return rgb2hex(rgbValues); - }, - - /** - * Number of steps which will be used as a cap when rounding colors. - * - * @memberof PIXI.CanvasTinter - * @type {number} - */ - cacheStepsPerColorChannel: 8, - - /** - * Tint cache boolean flag. - * - * @memberof PIXI.CanvasTinter - * @type {boolean} - */ - convertTintToImage: false, - - /** - * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. - * - * @memberof PIXI.CanvasTinter - * @type {boolean} - */ - canUseMultiply: canUseNewCanvasBlendModes(), - - /** - * The tinting method that will be used. - * - * @memberof PIXI.CanvasTinter - * @type {Function} - */ - tintMethod: () => - { // jslint-disable no-empty-function - - }, -}; - -CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; - -export default CanvasTinter; diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index 9126ed3..4bef51d 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -1,6 +1,22 @@ import { Sprite } from '@pixi/sprite'; /** + * Cached tinted texture. + * @memberof PIXI.Sprite# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +Sprite.prototype._tintedCanvas = null; + +/** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.Sprite# + * @member {number} _cachedTint + * @protected + */ +Sprite.prototype._cachedTint = 0xFFFFFF; + +/** * Renders the object using the Canvas renderer * * @private diff --git a/bundles/pixi.js-legacy/src/index.js b/bundles/pixi.js-legacy/src/index.js index f71fdb7..b4def4e 100644 --- a/bundles/pixi.js-legacy/src/index.js +++ b/bundles/pixi.js-legacy/src/index.js @@ -1,10 +1,10 @@ import '@pixi/polyfill'; import { Application, accessibility, interaction, prepare, extract } from 'pixi.js'; -import { autoDetectRenderer, CanvasRenderer } from '@pixi/canvas-renderer'; +import { autoDetectRenderer, CanvasRenderer, CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasMeshRenderer } from '@pixi/canvas-mesh'; import { CanvasGraphicsRenderer } from '@pixi/canvas-graphics'; -import { CanvasSpriteRenderer, CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasSpriteRenderer } from '@pixi/canvas-sprite'; import * as canvasExtract from '@pixi/canvas-extract'; import * as canvasPrepare from '@pixi/canvas-prepare'; import '@pixi/canvas-sprite-tiling'; diff --git a/packages/canvas/canvas-mesh/package.json b/packages/canvas/canvas-mesh/package.json index 807ac4c..180aaae 100644 --- a/packages/canvas/canvas-mesh/package.json +++ b/packages/canvas/canvas-mesh/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/constants": "^5.0.0-rc", "@pixi/mesh": "^5.0.0-rc", "@pixi/mesh-extras": "^5.0.0-rc", diff --git a/packages/canvas/canvas-mesh/src/NineSlicePlane.js b/packages/canvas/canvas-mesh/src/NineSlicePlane.js index f75e44f..d5ce07f 100644 --- a/packages/canvas/canvas-mesh/src/NineSlicePlane.js +++ b/packages/canvas/canvas-mesh/src/NineSlicePlane.js @@ -1,6 +1,31 @@ +import { CanvasTinter } from '@pixi/canvas-renderer'; import { NineSlicePlane } from '@pixi/mesh-extras'; /** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.NineSlicePlane# + * @member {number} _cachedTint + * @protected + */ +NineSlicePlane.prototype._cachedTint = 0xFFFFFF; + +/** + * Cached tinted texture. + * @memberof PIXI.NineSlicePlane# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +NineSlicePlane.prototype._tintedCanvas = null; + +/** + * Temporary storage for canvas source coords + * @memberof PIXI.NineSlicePlane# + * @member {number[]} _canvasUvs + * @private + */ +NineSlicePlane.prototype._canvasUvs = null; + +/** * Renders the object using the Canvas renderer * * @private @@ -11,13 +36,55 @@ NineSlicePlane.prototype._renderCanvas = function _renderCanvas(renderer) { const context = renderer.context; + const transform = this.worldTransform; + const res = renderer.resolution; + const isTinted = this.tint !== 0xFFFFFF; + const texture = this._texture; + + // Work out tinting + if (isTinted) + { + if (this._cachedTint !== this.tint) + { + // Tint has changed, need to update the tinted texture and use that instead + + this._cachedTint = this.tint; + + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + } + } + + const textureSource = !isTinted ? texture.baseTexture.source : this._tintedCanvas; + + if (!this._canvasUvs) + { + this._canvasUvs = [0, 0, 0, 0, 0, 0, 0, 0]; + } + + const vertices = this.vertices; + const uvs = this._canvasUvs; + const u0 = isTinted ? 0 : texture.frame.x; + const v0 = isTinted ? 0 : texture.frame.y; + const u1 = u0 + texture.frame.width; + const v1 = v0 + texture.frame.height; + + uvs[0] = u0; + uvs[1] = u0 + this._leftWidth; + uvs[2] = u1 - this._rightWidth; + uvs[3] = u1; + uvs[4] = v0; + uvs[5] = v0 + this._topHeight; + uvs[6] = v1 - this._bottomHeight; + uvs[7] = v1; + + for (let i = 0; i < 8; i++) + { + uvs[i] *= texture.baseTexture.resolution; + } context.globalAlpha = this.worldAlpha; renderer.setBlendMode(this.blendMode); - const transform = this.worldTransform; - const res = renderer.resolution; - if (this.roundPixels) { context.setTransform( @@ -41,71 +108,18 @@ ); } - const base = this._texture.baseTexture; - const textureSource = base.getDrawableSource(); - const w = base.width * base.resolution; - const h = base.height * base.resolution; - - this.drawSegment(context, textureSource, w, h, 0, 1, 10, 11); - this.drawSegment(context, textureSource, w, h, 2, 3, 12, 13); - this.drawSegment(context, textureSource, w, h, 4, 5, 14, 15); - this.drawSegment(context, textureSource, w, h, 8, 9, 18, 19); - this.drawSegment(context, textureSource, w, h, 10, 11, 20, 21); - this.drawSegment(context, textureSource, w, h, 12, 13, 22, 23); - this.drawSegment(context, textureSource, w, h, 16, 17, 26, 27); - this.drawSegment(context, textureSource, w, h, 18, 19, 28, 29); - this.drawSegment(context, textureSource, w, h, 20, 21, 30, 31); -}; - -/** - * Renders one segment of the plane. - * to mimic the exact drawing behavior of stretching the image like WebGL does, we need to make sure - * that the source area is at least 1 pixel in size, otherwise nothing gets drawn when a slice size of 0 is used. - * - * @method drawSegment - * @memberof PIXI.NineSlicePlane# - * @param {CanvasRenderingContext2D} context - The context to draw with. - * @param {ICanvasImageSource} textureSource - The source to draw. - * @param {number} w - width of the texture - * @param {number} h - height of the texture - * @param {number} x1 - x index 1 - * @param {number} y1 - y index 1 - * @param {number} x2 - x index 2 - * @param {number} y2 - y index 2 - */ -NineSlicePlane.prototype.drawSegment = function drawSegment(context, textureSource, w, h, x1, y1, x2, y2) -{ - // otherwise you get weird results when using slices of that are 0 wide or high. - const uvs = this.uvs; - const vertices = this.vertices; - - let sw = (uvs[x2] - uvs[x1]) * w; - let sh = (uvs[y2] - uvs[y1]) * h; - let dw = vertices[x2] - vertices[x1]; - let dh = vertices[y2] - vertices[y1]; - - // make sure the source is at least 1 pixel wide and high, otherwise nothing will be drawn. - if (sw < 1) + for (let row = 0; row < 3; row++) { - sw = 1; - } + for (let col = 0; col < 3; col++) + { + const ind = (col * 2) + (row * 8); + const sw = Math.max(1, uvs[col + 1] - uvs[col]); + const sh = Math.max(1, uvs[row + 5] - uvs[row + 4]); + const dw = Math.max(1, vertices[ind + 10] - vertices[ind]); + const dh = Math.max(1, vertices[ind + 11] - vertices[ind + 1]); - if (sh < 1) - { - sh = 1; + context.drawImage(textureSource, uvs[col], uvs[row + 4], sw, sh, + vertices[ind], vertices[ind + 1], dw, dh); + } } - - // make sure destination is at least 1 pixel wide and high, otherwise you get - // lines when rendering close to original size. - if (dw < 1) - { - dw = 1; - } - - if (dh < 1) - { - dh = 1; - } - - context.drawImage(textureSource, uvs[x1] * w, uvs[y1] * h, sw, sh, vertices[x1], vertices[y1], dw, dh); }; diff --git a/packages/canvas/canvas-renderer/src/CanvasTinter.js b/packages/canvas/canvas-renderer/src/CanvasTinter.js new file mode 100644 index 0000000..6ea164b --- /dev/null +++ b/packages/canvas/canvas-renderer/src/CanvasTinter.js @@ -0,0 +1,290 @@ +import { hex2rgb, rgb2hex } from '@pixi/utils'; +import canUseNewCanvasBlendModes from './utils/canUseNewCanvasBlendModes'; + +/** + * Utility methods for Sprite/Texture tinting. + * + * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, + * so be aware of the performance implications. + * + * @class + * @memberof PIXI + */ +const CanvasTinter = { + /** + * Basically this method just needs a sprite and a color and tints the sprite with the given color. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Sprite} sprite - the sprite to tint + * @param {number} color - the color to use to tint the sprite with + * @return {HTMLCanvasElement} The tinted canvas + */ + getTintedCanvas: (sprite, color) => + { + const texture = sprite._texture; + + color = CanvasTinter.roundColor(color); + + const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + texture.tintCache = texture.tintCache || {}; + + const cachedCanvas = texture.tintCache[stringColor]; + + let canvas; + + if (cachedCanvas) + { + if (cachedCanvas.tintId === texture._updateID) + { + return texture.tintCache[stringColor]; + } + + canvas = texture.tintCache[stringColor]; + } + else + { + canvas = CanvasTinter.canvas || document.createElement('canvas'); + } + + CanvasTinter.tintMethod(texture, color, canvas); + + canvas.tintId = texture._updateID; + + if (CanvasTinter.convertTintToImage) + { + // is this better? + const tintImage = new Image(); + + tintImage.src = canvas.toDataURL(); + + texture.tintCache[stringColor] = tintImage; + } + else + { + texture.tintCache[stringColor] = canvas; + // if we are not converting the texture to an image then we need to lose the reference to the canvas + CanvasTinter.canvas = null; + } + + return canvas; + }, + + /** + * Tint a texture using the 'multiply' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithMultiply: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'multiply'; + + const source = texture.baseTexture.getDrawableSource(); + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + context.globalCompositeOperation = 'destination-atop'; + + context.drawImage( + source, + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + }, + + /** + * Tint a texture using the 'overlay' operation. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithOverlay(texture, color, canvas) + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; + context.fillRect(0, 0, crop.width, crop.height); + + context.globalCompositeOperation = 'destination-atop'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + + // context.globalCompositeOperation = 'copy'; + context.restore(); + }, + + /** + * Tint a texture pixel per pixel. + * + * @memberof PIXI.CanvasTinter + * @param {PIXI.Texture} texture - the texture to tint + * @param {number} color - the color to use to tint the sprite with + * @param {HTMLCanvasElement} canvas - the current canvas + */ + tintWithPerPixel: (texture, color, canvas) => + { + const context = canvas.getContext('2d'); + const crop = texture._frame.clone(); + const resolution = texture.baseTexture.resolution; + + crop.x *= resolution; + crop.y *= resolution; + crop.width *= resolution; + crop.height *= resolution; + + canvas.width = Math.ceil(crop.width); + canvas.height = Math.ceil(crop.height); + + context.save(); + context.globalCompositeOperation = 'copy'; + context.drawImage( + texture.baseTexture.getDrawableSource(), + crop.x, + crop.y, + crop.width, + crop.height, + 0, + 0, + crop.width, + crop.height + ); + context.restore(); + + const rgbValues = hex2rgb(color); + const r = rgbValues[0]; + const g = rgbValues[1]; + const b = rgbValues[2]; + + const pixelData = context.getImageData(0, 0, crop.width, crop.height); + + const pixels = pixelData.data; + + for (let i = 0; i < pixels.length; i += 4) + { + pixels[i + 0] *= r; + pixels[i + 1] *= g; + pixels[i + 2] *= b; + } + + context.putImageData(pixelData, 0, 0); + }, + + /** + * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. + * + * @memberof PIXI.CanvasTinter + * @param {number} color - the color to round, should be a hex color + * @return {number} The rounded color. + */ + roundColor: (color) => + { + const step = CanvasTinter.cacheStepsPerColorChannel; + + const rgbValues = hex2rgb(color); + + rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); + rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); + rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); + + return rgb2hex(rgbValues); + }, + + /** + * Number of steps which will be used as a cap when rounding colors. + * + * @memberof PIXI.CanvasTinter + * @type {number} + */ + cacheStepsPerColorChannel: 8, + + /** + * Tint cache boolean flag. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + convertTintToImage: false, + + /** + * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. + * + * @memberof PIXI.CanvasTinter + * @type {boolean} + */ + canUseMultiply: canUseNewCanvasBlendModes(), + + /** + * The tinting method that will be used. + * + * @memberof PIXI.CanvasTinter + * @type {Function} + */ + tintMethod: () => + { // jslint-disable no-empty-function + + }, +}; + +CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; + +export default CanvasTinter; diff --git a/packages/canvas/canvas-renderer/src/index.js b/packages/canvas/canvas-renderer/src/index.js index 1bf7e9b..a8fd078 100644 --- a/packages/canvas/canvas-renderer/src/index.js +++ b/packages/canvas/canvas-renderer/src/index.js @@ -1,5 +1,6 @@ export { default as CanvasRenderer } from './CanvasRenderer'; export { default as canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes'; export { autoDetectRenderer } from './autoDetectRenderer'; +export { default as CanvasTinter } from './CanvasTinter'; import './BaseTexture'; diff --git a/packages/canvas/canvas-sprite-tiling/package.json b/packages/canvas/canvas-sprite-tiling/package.json index 27ec81a..f572074 100644 --- a/packages/canvas/canvas-sprite-tiling/package.json +++ b/packages/canvas/canvas-sprite-tiling/package.json @@ -22,6 +22,7 @@ "lib" ], "dependencies": { + "@pixi/canvas-renderer": "^5.0.0-rc", "@pixi/canvas-sprite": "^5.0.0-rc", "@pixi/sprite-tiling": "^5.0.0-rc", "@pixi/utils": "^5.0.0-rc" diff --git a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js index 8db6ed9..8f4e0e4 100644 --- a/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js +++ b/packages/canvas/canvas-sprite-tiling/src/TilingSprite.js @@ -1,5 +1,5 @@ import { TilingSprite } from '@pixi/sprite-tiling'; -import { CanvasTinter } from '@pixi/canvas-sprite'; +import { CanvasTinter } from '@pixi/canvas-renderer'; import { CanvasRenderTarget } from '@pixi/utils'; /** @@ -40,8 +40,8 @@ // Tint the tiling sprite if (this.tint !== 0xFFFFFF) { - this.tintedTexture = CanvasTinter.getTintedTexture(this, this.tint); - tempCanvas.context.drawImage(this.tintedTexture, 0, 0); + this._tintedCanvas = CanvasTinter.getTintedCanvas(this, this.tint); + tempCanvas.context.drawImage(this._tintedCanvas, 0, 0); } else { diff --git a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js index ebf7397..d95fde5 100644 --- a/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js +++ b/packages/canvas/canvas-sprite/src/CanvasSpriteRenderer.js @@ -1,6 +1,6 @@ import { SCALE_MODES, BLEND_MODES } from '@pixi/constants'; import { Matrix, GroupD8 } from '@pixi/math'; -import CanvasTinter from './CanvasTinter'; +import { CanvasTinter } from '@pixi/canvas-renderer'; const canvasRenderWorldTransform = new Matrix(); @@ -149,12 +149,12 @@ if (sprite.tint !== 0xFFFFFF) { - if (sprite.cachedTint !== sprite.tint || sprite.tintedTexture.tintId !== sprite._texture._updateID) + if (sprite._cachedTint !== sprite.tint || sprite._tintedCanvas.tintId !== sprite._texture._updateID) { - sprite.cachedTint = sprite.tint; + sprite._cachedTint = sprite.tint; // TODO clean up caching - how to clean up the caches? - sprite.tintedTexture = CanvasTinter.getTintedTexture(sprite, sprite.tint); + sprite._tintedCanvas = CanvasTinter.getTintedCanvas(sprite, sprite.tint); } context.drawImage( diff --git a/packages/canvas/canvas-sprite/src/CanvasTinter.js b/packages/canvas/canvas-sprite/src/CanvasTinter.js deleted file mode 100644 index 98c72e9..0000000 --- a/packages/canvas/canvas-sprite/src/CanvasTinter.js +++ /dev/null @@ -1,290 +0,0 @@ -import { hex2rgb, rgb2hex } from '@pixi/utils'; -import { canUseNewCanvasBlendModes } from '@pixi/canvas-renderer'; - -/** - * Utility methods for Sprite/Texture tinting. - * - * Tinting with the CanvasRenderer involves creating a new canvas to use as a texture, - * so be aware of the performance implications. - * - * @class - * @memberof PIXI - */ -const CanvasTinter = { - /** - * Basically this method just needs a sprite and a color and tints the sprite with the given color. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Sprite} sprite - the sprite to tint - * @param {number} color - the color to use to tint the sprite with - * @return {HTMLCanvasElement} The tinted canvas - */ - getTintedTexture: (sprite, color) => - { - const texture = sprite._texture; - - color = CanvasTinter.roundColor(color); - - const stringColor = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - - texture.tintCache = texture.tintCache || {}; - - const cachedTexture = texture.tintCache[stringColor]; - - let canvas; - - if (cachedTexture) - { - if (cachedTexture.tintId === texture._updateID) - { - return texture.tintCache[stringColor]; - } - - canvas = texture.tintCache[stringColor]; - } - else - { - canvas = CanvasTinter.canvas || document.createElement('canvas'); - } - - CanvasTinter.tintMethod(texture, color, canvas); - - canvas.tintId = texture._updateID; - - if (CanvasTinter.convertTintToImage) - { - // is this better? - const tintImage = new Image(); - - tintImage.src = canvas.toDataURL(); - - texture.tintCache[stringColor] = tintImage; - } - else - { - texture.tintCache[stringColor] = canvas; - // if we are not converting the texture to an image then we need to lose the reference to the canvas - CanvasTinter.canvas = null; - } - - return canvas; - }, - - /** - * Tint a texture using the 'multiply' operation. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithMultiply: (texture, color, canvas) => - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - - context.fillRect(0, 0, crop.width, crop.height); - - context.globalCompositeOperation = 'multiply'; - - const source = texture.baseTexture.getDrawableSource(); - - context.drawImage( - source, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - - context.globalCompositeOperation = 'destination-atop'; - - context.drawImage( - source, - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - context.restore(); - }, - - /** - * Tint a texture using the 'overlay' operation. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithOverlay(texture, color, canvas) - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.globalCompositeOperation = 'copy'; - context.fillStyle = `#${(`00000${(color | 0).toString(16)}`).substr(-6)}`; - context.fillRect(0, 0, crop.width, crop.height); - - context.globalCompositeOperation = 'destination-atop'; - context.drawImage( - texture.baseTexture.getDrawableSource(), - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - - // context.globalCompositeOperation = 'copy'; - context.restore(); - }, - - /** - * Tint a texture pixel per pixel. - * - * @memberof PIXI.CanvasTinter - * @param {PIXI.Texture} texture - the texture to tint - * @param {number} color - the color to use to tint the sprite with - * @param {HTMLCanvasElement} canvas - the current canvas - */ - tintWithPerPixel: (texture, color, canvas) => - { - const context = canvas.getContext('2d'); - const crop = texture._frame.clone(); - const resolution = texture.baseTexture.resolution; - - crop.x *= resolution; - crop.y *= resolution; - crop.width *= resolution; - crop.height *= resolution; - - canvas.width = Math.ceil(crop.width); - canvas.height = Math.ceil(crop.height); - - context.save(); - context.globalCompositeOperation = 'copy'; - context.drawImage( - texture.baseTexture.getDrawableSource(), - crop.x, - crop.y, - crop.width, - crop.height, - 0, - 0, - crop.width, - crop.height - ); - context.restore(); - - const rgbValues = hex2rgb(color); - const r = rgbValues[0]; - const g = rgbValues[1]; - const b = rgbValues[2]; - - const pixelData = context.getImageData(0, 0, crop.width, crop.height); - - const pixels = pixelData.data; - - for (let i = 0; i < pixels.length; i += 4) - { - pixels[i + 0] *= r; - pixels[i + 1] *= g; - pixels[i + 2] *= b; - } - - context.putImageData(pixelData, 0, 0); - }, - - /** - * Rounds the specified color according to the CanvasTinter.cacheStepsPerColorChannel. - * - * @memberof PIXI.CanvasTinter - * @param {number} color - the color to round, should be a hex color - * @return {number} The rounded color. - */ - roundColor: (color) => - { - const step = CanvasTinter.cacheStepsPerColorChannel; - - const rgbValues = hex2rgb(color); - - rgbValues[0] = Math.min(255, (rgbValues[0] / step) * step); - rgbValues[1] = Math.min(255, (rgbValues[1] / step) * step); - rgbValues[2] = Math.min(255, (rgbValues[2] / step) * step); - - return rgb2hex(rgbValues); - }, - - /** - * Number of steps which will be used as a cap when rounding colors. - * - * @memberof PIXI.CanvasTinter - * @type {number} - */ - cacheStepsPerColorChannel: 8, - - /** - * Tint cache boolean flag. - * - * @memberof PIXI.CanvasTinter - * @type {boolean} - */ - convertTintToImage: false, - - /** - * Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method. - * - * @memberof PIXI.CanvasTinter - * @type {boolean} - */ - canUseMultiply: canUseNewCanvasBlendModes(), - - /** - * The tinting method that will be used. - * - * @memberof PIXI.CanvasTinter - * @type {Function} - */ - tintMethod: () => - { // jslint-disable no-empty-function - - }, -}; - -CanvasTinter.tintMethod = CanvasTinter.canUseMultiply ? CanvasTinter.tintWithMultiply : CanvasTinter.tintWithPerPixel; - -export default CanvasTinter; diff --git a/packages/canvas/canvas-sprite/src/Sprite.js b/packages/canvas/canvas-sprite/src/Sprite.js index 9126ed3..4bef51d 100644 --- a/packages/canvas/canvas-sprite/src/Sprite.js +++ b/packages/canvas/canvas-sprite/src/Sprite.js @@ -1,6 +1,22 @@ import { Sprite } from '@pixi/sprite'; /** + * Cached tinted texture. + * @memberof PIXI.Sprite# + * @member {HTMLCanvasElement} _tintedCanvas + * @protected + */ +Sprite.prototype._tintedCanvas = null; + +/** + * Cached tint value so we can tell when the tint is changed. + * @memberof PIXI.Sprite# + * @member {number} _cachedTint + * @protected + */ +Sprite.prototype._cachedTint = 0xFFFFFF; + +/** * Renders the object using the Canvas renderer * * @private diff --git a/packages/canvas/canvas-sprite/src/index.js b/packages/canvas/canvas-sprite/src/index.js index 87cb6ea..c99e2df 100644 --- a/packages/canvas/canvas-sprite/src/index.js +++ b/packages/canvas/canvas-sprite/src/index.js @@ -1,4 +1,3 @@ export { default as CanvasSpriteRenderer } from './CanvasSpriteRenderer'; -export { default as CanvasTinter } from './CanvasTinter'; import './Sprite';