我正在研究一个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的颜色也应为灰色,等等。
我知道我可以获取精灵的图像数据,遍历每个像素并将黑色像素分别更改为不同的值,如下所示:
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个),并且在游戏循环中分别对每个精灵进行所有这些操作似乎效率很低。
做我想完成的最好的方法是什么?
答案 0 :(得分:0)
对于黑色键,有一个不错的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>
但是,如果确实如此,那么您甚至可以考虑直接从缩减采样后的画布上绘制所有图形,并且仅在最后一步调整大小。