Изменение цвета «черных пикселей» спрайтов в HTML5 Canvas - PullRequest
0 голосов
/ 21 марта 2019

Я работаю над небольшим проектом HTML5 Canvas. Во время игры мне иногда нужно заставить черные пиксели нескольких спрайтов постоянно «светиться» от чистого черного до светло-серого и снова возвращаться к чистому черному (затем повторяться некоторое время).

Это происходит в несколько этапов (всего 7), так что получается что-то вроде чистого черного -> темно-серый -> серый -> и т. Д.

Ни один из пикселей спрайтов, кроме черных, не должен быть затронут.

Вот пример анимации спрайта

Вот пример того, как должно выглядеть светящееся

Это должно происходить постоянно, пока сам спрайт анимируется. Свечение происходит отдельно от таймера анимации спрайта, поэтому в любой момент у меня может быть любая комбинация рамки анимации и «цвета свечения».

Кроме того, таймер свечения одинаков для всех спрайтов, на которые он влияет. Так что, если спрайт 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+), и выполнение всего этого для каждого из них в отдельности в игровом цикле кажется довольно неэффективным.

Какой лучший способ сделать то, что я пытаюсь сделать?

1 Ответ

0 голосов
/ 21 марта 2019

Для черного хроматического ключа есть хороший SVG-фильтр, который позволяет преобразовывать яркость в альфа.

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">

Хорошо, но также влияет на другие цвета (белый => непрозрачный, серый => полупрозрачный, черный => прозрачный).
Поэтому нам нужно применить другой фильтр, который будет действовать как пороговое значение для альфа-канала.

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() должно быть проще для оптимизации для браузеров, чем загрузка процессора 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>

Но если это действительно так, то вы можете даже подумать о том, чтобы сделать все свои рисунки непосредственно с холста с уменьшенной выборкой, и изменить размер только в качестве последнего шага.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...