我正在研究一个小小的HTML5 Canvas游戏项目。在游戏过程中,我有时需要让多个精灵的黑色像素不断“发光”,从纯黑色到浅灰色,再回到纯黑色(然后重复一段时间)。
这发生在多个步骤(总共7个),所以它像纯黑色 - >深灰色 - >灰色 - >等。
除了黑色像素之外,所有精灵的像素都不应受到影响。
Here's an example of a sprite's animation
Here's an example of what the glowing should look like
这必须在精灵本身动画时不断发生。发光与sprite的动画在一个单独的计时器上,所以在任何时候我都可以拥有动画帧和“发光颜色”的任意组合。
此外,发光计时器对于受其影响的所有精灵都是相同的。因此,如果精灵1的发光亮灰色,那么精灵2,精灵3等等。
我知道我可以获得精灵的图像数据,遍历每个像素并将黑色像素单独更改为不同的值,执行以下操作:
var map = ctx.getImageData(sprite.x, sprite.y, sprite.width, sprite.height);
for (var p = 0; p < map.data.length; p += 4) {
if (map.data[p] === 0 && map.data[p + 1] === 0 && map.data[p + 2] === 0) {
// If black pixel, swap with glow's current color
map.data[p] = map.data[p + 1] = map.data[p + 2] = currentGlow;
}
ctx.putImageData(map, sprite.x, sprite.y);
但我担心这会带来性能问题。我可能同时拥有大量不同的精灵和发光效果(100+),并且在游戏循环中为每个人单独做这一切似乎效率很低。
做我想要完成的事情的最佳方式是什么?
对于黑色色度键,有一个很好的SVG滤镜,可以将亮度转换为alpha。
img { filter: url(#getBlack); }
body { background: salmon; }
<svg width="0" height="0">
<defs>
<filter id="getBlack">
<feColorMatrix type="luminanceToAlpha"/>
</filter>
</defs>
</svg>
<img src="https://i.stack.imgur.com/Cg1PS.png">
很好,但它也会影响其他颜色(白色=>不透明,灰色=>半透明,黑色=>透明)。 所以我们需要应用另一个过滤器,它将作为Alpha通道的阈值。
img { filter: url(#getBlack); }
body { background: salmon; }
<svg width="0" height="0">
<defs>
<filter id="getBlack">
<feColorMatrix type="luminanceToAlpha"/>
<feComponentTransfer>
<feFuncA type="linear" slope="255"/>
</feComponentTransfer>
</filter>
</defs>
</svg>
<img src="https://i.stack.imgur.com/Cg1PS.png">
但现在我们所有的像素都是黑色的,除非有黑色像素......这仍然不是我们需要的。 好吧,我们可以使用一些合成(我们将从Canvas API中完成)来实现我们的目标:
const ctx = canvas.getContext('2d');
const img = new Image;
let color = 0;
let inc = 2;
img.onload = init;
img.src = "https://i.stack.imgur.com/Cg1PS.png";
function init() {
canvas.width = img.width;
canvas.height = img.height;
draw();
}
function draw() {
// clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// first pass, draw the scene normally
ctx.drawImage(img, 0, 0);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 50, 50);
// second pass, keep only non-black pixels
ctx.globalCompositeOperation = "destination-atop";
ctx.filter = 'url(#getBlack)';
ctx.drawImage(canvas, 0, 0); // use the canvas as source
ctx.filter = 'none';
// third pass, draw behind actual value for were-black
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = "#" + (color.toString(16).padStart(2, '0')).repeat(3)
ctx.fillRect(0, 0, canvas.width, canvas.height);
// clean
ctx.globalCompositeOperation = "source-over";
// dirty increment color
if (
((color += inc) > 255 && (color = 255)) ||
(color < 0 && !(color = 0))
) {
inc *= -1;
}
requestAnimationFrame(draw);
}
body { background: salmon; }
<svg width="0" height="0">
<defs>
<filter id="getBlack">
<feColorMatrix type="luminanceToAlpha"/>
<feComponentTransfer>
<feFuncA type="linear" slope="255"/>
</feComponentTransfer>
</filter>
</defs>
</svg>
<canvas id="canvas" style="background: salmon"></canvas>
现在,虽然filter()
应该比CPU密集型getImageData
更容易针对浏览器进行优化,但我没有进行大量测试来确认或证实这一点,我怀疑不同的输入无论如何都会显示不同的结果,所以最后,你负责为您的应用选择最佳方式。
但无论如何,你可能需要执行ImageData操作,因为遗憾的是filter
在任何地方都不受支持,所以你可能想要考虑的一件事,而且当看起来你正在使用像素艺术的精灵时,是下采样。
您可以在较小的画布上重现画布,在较小的版本上执行色度键,并在子采样的色度结果后使用合成,而不是在全尺寸画布上进行色度键。
const ctx = canvas.getContext('2d');
const img = new Image;
let color = 0;
let inc = 2;
img.onload = init;
img.crossOrigin = 'anonymous';
img.src = "https://upload.wikimedia.org/wikipedia/commons/b/b1/Monochrome_pixelated_head.png";
const smallCanvas = document.createElement('canvas');
const smallCtx = smallCanvas.getContext('2d');
document.body.append(smallCanvas);
function init() {
canvas.width = img.width * 5;
canvas.height = img.height * 5;
smallCanvas.width = canvas.width / 10;
smallCanvas.height = canvas.height / 10;
ctx.imageSmoothingEnabled = false;
smallCtx.imageSmoothingEnabled = false;
draw();
}
function draw() {
// clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
smallCtx.clearRect(0, 0, smallCanvas.width, smallCanvas.height);
// first pass, draw the scene normally
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 50, 50);
// second pass, do the chroma-key on the small canvas
smallCtx.drawImage(canvas, 0, 0, smallCanvas.width, smallCanvas.height); // use the canvas as source
// get the small ImageData
const data = smallCtx.getImageData(0, 0, smallCanvas.width, smallCanvas.height);
// by using an Uint32Array, we can work one pixel at a time
// (4 times less iterations than with Uint8Clamped)
const arr = new Uint32Array(data.data.buffer);
const currentColor = parseInt('FF' + (color.toString(16).padStart(2, '0')).repeat(3), 16);
for (let i = 0; i < arr.length; i++) {
if (arr[i] === 0xFF000000) arr[i] = currentColor;
}
// apply modified ImageData
smallCtx.putImageData(data, 0, 0);
// now draw big
ctx.drawImage(smallCanvas, 0, 0, canvas.width, canvas.height);
// dirty increment color
if (
((color += inc) > 255 && (color = 255)) ||
(color < 0 && !(color = 0))
) {
inc *= -1;
}
requestAnimationFrame(draw);
}
body { background: salmon; }
<svg width="0" height="0">
<defs>
<filter id="getBlack">
<feColorMatrix type="luminanceToAlpha"/>
<feComponentTransfer>
<feFuncA type="linear" slope="255"/>
</feComponentTransfer>
</filter>
</defs>
</svg>
<canvas id="canvas" style="background: salmon"></canvas>
但如果情况确实如此,那么您甚至可以考虑直接从缩减采样画布中完成所有绘图,并仅在最后一步调整大小。