import { Color, settings } from '@pixi/core';
import { canUseNewCanvasBlendModes } from './utils/canUseNewCanvasBlendModes';
import type { ICanvas, Texture } from '@pixi/core';
/**
* 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.
* @namespace PIXI.canvasUtils
* @memberof PIXI
*/
export const canvasUtils = {
canvas: null as ICanvas,
/**
* Basically this method just needs a sprite and a color and tints the sprite with the given color.
* @memberof PIXI.canvasUtils
* @param {PIXI.Sprite} sprite - the sprite to tint
* @param sprite.texture
* @param {number} color - the color to use to tint the sprite with
* @returns {ICanvas|HTMLImageElement} The tinted canvas
*/
getTintedCanvas: (sprite: { texture: Texture }, color: number): ICanvas | HTMLImageElement =>
{
const texture = sprite.texture;
color = canvasUtils.roundColor(color);
const stringColor = `#${(`00000${(color | 0).toString(16)}`).slice(-6)}`;
texture.tintCache = texture.tintCache || {};
const cachedCanvas = texture.tintCache[stringColor];
let canvas: ICanvas;
if (cachedCanvas)
{
if (cachedCanvas.tintId === texture._updateID)
{
return texture.tintCache[stringColor];
}
canvas = texture.tintCache[stringColor] as ICanvas;
}
else
{
canvas = settings.ADAPTER.createCanvas();
}
canvasUtils.tintMethod(texture, color, canvas);
canvas.tintId = texture._updateID;
// Convert tint to image only if ICanvas.toDataURL exists (e.g. OffscreenCanvas don't have toDataURL)
if (canvasUtils.convertTintToImage && canvas.toDataURL !== undefined)
{
// is this better?
const tintImage = new Image();
tintImage.src = canvas.toDataURL();
texture.tintCache[stringColor] = tintImage;
}
else
{
texture.tintCache[stringColor] = canvas;
}
return canvas;
},
/**
* Basically this method just needs a sprite and a color and tints the sprite with the given color.
* @memberof PIXI.canvasUtils
* @param {PIXI.Texture} texture - the sprite to tint
* @param {number} color - the color to use to tint the sprite with
* @returns {CanvasPattern} The tinted canvas
*/
getTintedPattern: (texture: Texture, color: number): CanvasPattern =>
{
color = canvasUtils.roundColor(color);
const stringColor = `#${(`00000${(color | 0).toString(16)}`).slice(-6)}`;
texture.patternCache = texture.patternCache || {};
let pattern = texture.patternCache[stringColor];
if (pattern?.tintId === texture._updateID)
{
return pattern;
}
if (!canvasUtils.canvas)
{
canvasUtils.canvas = settings.ADAPTER.createCanvas();
}
canvasUtils.tintMethod(texture, color, canvasUtils.canvas);
pattern = canvasUtils.canvas.getContext('2d').createPattern(canvasUtils.canvas, 'repeat');
pattern.tintId = texture._updateID;
texture.patternCache[stringColor] = pattern;
return pattern;
},
/**
* Tint a texture using the 'multiply' operation.
* @memberof PIXI.canvasUtils
* @param {PIXI.Texture} texture - the texture to tint
* @param {number} color - the color to use to tint the sprite with
* @param {PIXI.ICanvas} canvas - the current canvas
*/
tintWithMultiply: (texture: Texture, color: number, canvas: ICanvas): void =>
{
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)}`).slice(-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.canvasUtils
* @param {PIXI.Texture} texture - the texture to tint
* @param {number} color - the color to use to tint the sprite with
* @param {PIXI.ICanvas} canvas - the current canvas
*/
tintWithOverlay: (texture: Texture, color: number, canvas: ICanvas): void =>
{
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)}`).slice(-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.canvasUtils
* @param {PIXI.Texture} texture - the texture to tint
* @param {number} color - the color to use to tint the sprite with
* @param {PIXI.ICanvas} canvas - the current canvas
*/
tintWithPerPixel: (texture: Texture, color: number, canvas: ICanvas): void =>
{
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 [r, g, b] = Color.shared.setValue(color).toArray();
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 canvasUtils.cacheStepsPerColorChannel.
* @memberof PIXI.canvasUtils
* @param {number} color - the color to round, should be a hex color
* @returns {number} The rounded color.
*/
roundColor: (color: number): number =>
Color.shared
.setValue(color)
.round(canvasUtils.cacheStepsPerColorChannel)
.toNumber(),
/**
* Number of steps which will be used as a cap when rounding colors.
* @memberof PIXI.canvasUtils
* @type {number}
*/
cacheStepsPerColorChannel: 8,
/**
* Tint cache boolean flag.
* @memberof PIXI.canvasUtils
* @type {boolean}
*/
convertTintToImage: false,
/**
* Whether or not the Canvas BlendModes are supported, consequently the ability to tint using the multiply method.
* @memberof PIXI.canvasUtils
* @type {boolean}
*/
canUseMultiply: canUseNewCanvasBlendModes(),
/**
* The tinting method that will be used.
* @memberof PIXI.canvasUtils
* @type {Function}
*/
tintMethod: null as (texture: Texture, color: number, canvas: ICanvas) => void,
};
canvasUtils.tintMethod = canvasUtils.canUseMultiply ? canvasUtils.tintWithMultiply : canvasUtils.tintWithPerPixel;