在HTML5 Canvas游戏中改变精灵的黑色像素颜色

问题描述 投票:0回答:1

我正在研究一个小小的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+),并且在游戏循环中为每个人单独做这一切似乎效率很低。

做我想要完成的事情的最佳方式是什么?

javascript html5 html5-canvas
1个回答
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>

但如果情况确实如此,那么您甚至可以考虑直接从缩减采样画布中完成所有绘图,并仅在最后一步调整大小。

© www.soinside.com 2019 - 2024. All rights reserved.