Почему моя функция холста всегда отображается красным цветом? - PullRequest
2 голосов
/ 06 июня 2019

Я пытаюсь применить шумовой эффект к своему холсту на основе codepen , который я видел, что, в свою очередь, похоже на ответ SO .

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


Я рефакторингкодировать код значительно, потому что (на данный момент) меня не волнует анимация шума:

/**
 * apply a "noise filter" to a rectangular region of the canvas
 * @param {Canvas2DContext} ctx - the context to draw on
 * @param {Number} x - the x-coordinate of the top-left corner of the region to noisify
 * @param {Number} y - the y-coordinate of the top-left corner of the region to noisify
 * @param {Number} width - how wide, in canvas units, the noisy region should be
 * @param {Number} height - how tall, in canvas units, the noisy region should be
 * @effect draws directly to the canvas
 */
function drawNoise( ctx, x, y, width, height ) {
    let imageData = ctx.createImageData(width, height)
    let buffer32 = new Uint32Array(imageData.data.buffer)

    for (let i = 0, len = buffer32.length; i < len; i++) {
        buffer32[i] = Math.random() < 0.5
            ? 0x00000088 // "noise" pixel
            : 0x00000000 // non-noise pixel
    }

    ctx.putImageData(imageData, x, y)
}

Из того, что я могу сказать, суть того, что происходит, заключается в том, что мы обертываем необработанный ImageDataпредставление данных (серия из 8-битных элементов, которые отражают значения красного, зеленого, синего и альфа-канала для каждого пикселя, последовательно) в 32-битном массиве, что позволяет нам работать с каждым пикселем в виде единого кортежа.Мы получаем массив с одним элементом на пиксель вместо четырех элементов на пиксель.

Затем мы перебираем элементы этого массива, записывая значения RGBA для каждого элемента (т.е. каждого пикселя) на основе нашей шумовой логики.Шумовая логика здесь действительно проста: каждый пиксель с вероятностью ~ 50% может быть пикселем «шума».

Пикселям шума назначается 32-битное значение 0x00000088, что (благодаря 32-разделение на биты, обеспечиваемое массивом) эквивалентно rgba(0, 0, 0, 0.5), т. е. черный цвет, непрозрачность 50%.

Пикселям без шума назначается 32-битное значение 0x00000000, то есть непрозрачность черного цвета 0%, т.е.полностью прозрачный.

Интересно, что мы не пишем buffer32 на холст.Вместо этого мы пишем imageData, который использовался для создания Uint32Array, что наводит меня на мысль, что мы мутируем объект imageData через какой-то переход по ссылке;Мне не совсем понятно, почему это так.Я знаю, как передача значений и ссылок обычно работает в JS (скаляры передаются по значению, объекты передаются по ссылке), но в нетипизированном мире массивов значение, передаваемое конструктору массива, просто определяет длину массива.Это, очевидно, не то, что здесь происходит.


Как уже отмечалось, вместо поля черных пикселей, которые прозрачны на 50% или 100%, я получаю поле всех сплошных пикселей, всех красных.Мало того, что я не ожидаю увидеть красный цвет, есть нулевое свидетельство случайного назначения цвета: каждый пиксель является сплошным красным.

Играя с двумя шестнадцатеричными значениями, яобнаружил, что это приводит к рассеянию красного на черном, которое имеет правильное распределение:

buffer32[i] = Math.random() < 0.5
    ? 0xff0000ff // <-- I'd assume this is solid red
    : 0xff000000 // <-- I'd assume this is invisible red

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

Смущает, что я не могу получить никаких цветов, кроме красного или черного.Я также не могу получить никакой прозрачности, кроме 100% непрозрачности.Просто чтобы проиллюстрировать разъединение, я удалил случайный элемент и попытался записать каждое из этих девяти значений в каждый пиксель, просто чтобы посмотреть, что происходит:

buffer32[i] = 0xRrGgBbAa
                         // EXPECTED   // ACTUAL
buffer32[i] = 0xff0000ff // red 100%   // red 100%
buffer32[i] = 0x00ff00ff // green 100% // red 100%
buffer32[i] = 0x0000ffff // blue 100%  // red 100%
buffer32[i] = 0xff000088 // red 50%    // blood red; could be red on black at 50%
buffer32[i] = 0x00ff0088 // green 50%  // red 100%
buffer32[i] = 0x0000ff88 // blue 50%   // red 100%
buffer32[i] = 0xff000000 // red 0%     // black 100%
buffer32[i] = 0x00ff0000 // green 0%   // red 100%
buffer32[i] = 0x0000ff00 // blue 0%    // red 100%

Что происходит?


РЕДАКТИРОВАТЬ: аналогичные (плохие) результаты после отказа от Uint32Array и жуткой мутации, основанной на статье MDN о ImageData.data:

/**
 * fails in exactly the same way
 */
function drawNoise( ctx, x, y, width, height ) {
    let imageData = ctx.createImageData(width, height)

    for (let i = 0, len = imageData.data.length; i < len; i += 4) {
        imageData.data[i + 0] = 0
        imageData.data[i + 1] = 0
        imageData.data[i + 2] = 0
        imageData.data[i + 3] = Math.random() < 0.5 ? 255 : 0
    }

    ctx.putImageData(imageData, x, y)
}

1 Ответ

2 голосов
/ 06 июня 2019

[TLDR]:

Порядковый номер вашего оборудования задан как LittleEndian, и поэтому правильный шестнадцатеричный формат равен 0xAABBGGRR, а не 0xRRGGBBAA.


Сначала давайте объясним " волшебство " позади TypedArrays: ArrayBuffers .

ArrayBuffer - это особый объект, который напрямую связан с памятью устройства. Сам по себе интерфейс ArrayBuffer не имеет для нас особых возможностей, но когда вы его создаете, вы фактически выделяете его length в памяти для своего собственного сценария. То есть движок js не справится с его перераспределением, перемещением в другое место, разбиением на части и всеми этими медленными операциями, как это происходит с обычными объектами JS.
Таким образом, это делает его одним из самых быстрых объектов для манипулирования двоичными данными.

Однако, как уже было сказано, его интерфейс сам по себе весьма ограничен. У нас нет способа получить доступ к данным напрямую из ArrayBuffer, для этого нам нужно использовать объект view , который не будет копировать данные, а просто предложит способ прямого доступа к ним.

Вы можете иметь разные представления для одного и того же ArrayBuffer, но используемые данные всегда будут просто данными ArrayBuffer, и если вы отредактируете ArrayBuffer из одного представления, то он будет виден из другого:

const buffer = new ArrayBuffer(4);
const view1 = new Uint8Array(buffer);
const view2 = new Uint8Array(buffer);

console.log('view1', ...view1); // [0,0,0,0]
console.log('view2', ...view2); // [0,0,0,0]

// we modify only view1
view1[2] = 125;

console.log('view1', ...view1); // [0,0,125,0]
console.log('view2', ...view2); // [0,0,125,0]

Существуют различные типы представления объектов, и каждый из них предлагает различные способы представления двоичных данных, которые назначены слоту памяти, выделенному ArrayBuffer.

TypedArrays , такие как Uint8Array, Float32Array и т. Д., Представляют собой интерфейсы ArrayLike, которые предлагают простой способ манипулировать данными как массивом, представляя данные в их собственном формате (8 бит, Float32 и т. Д.).
Интерфейс DataView допускает более открытые манипуляции, такие как чтение в различных форматах даже из обычно недопустимых границ, однако это происходит за счет производительности.

Интерфейс ImageData сам использует ArrayBuffer для хранения своих данных пикселей. По умолчанию он предоставляет Uint8ClampedArray представление поверх этих данных. То есть объект ArrayLike, в котором каждый 32-битный пиксель представлен в виде значений от 0 до 255 для каждого канала Red, Green, Blue и Alpha, в указанном порядке.

Таким образом, ваш код использует тот факт, что TypedArrays являются только представлениями объектов, а наличие другого представления над базовым ArrayBuffer изменит его напрямую.
Его автор решил использовать Uint32Array, потому что это способ установить полный пиксель (помните, холст 32 бит) в одном кадре. Вы можете сократить требуемую работу в четыре раза.

Однако, делая это, вы начинаете работать с 32-битными значениями. И это может быть немного проблематично, потому что теперь порядковый номер имеет значение.
Uint8Array [0x00, 0x11, 0x22, 0x33] будет представлен как 32-битное значение 0x00112233 в системах BigEndian, но как 0x33221100 в системах LittleEndian.

const buff = new ArrayBuffer(4);
const uint8 = new Uint8Array(buff);
const uint32 = new Uint32Array(buff);

uint8[0] = 0x00;
uint8[1] = 0x11;
uint8[2] = 0x22;
uint8[3] = 0x33;

const hex32 = uint32[0].toString(16);
console.log(hex32, hex32 === "33221100" ? 'LE' : 'BE');

Обратите внимание, что большинство персональных устройств - LittleEndian, поэтому неудивительно, если ваш компьютер тоже.


Итак, со всем этим, я надеюсь, вы знаете, как исправить свой код: чтобы сгенерировать цвет rgba(0,0,0,.5), вам нужно установить значение Uint32 0x80000000

drawNoise(canvas.getContext('2d'), 0, 0, 300, 150);

function drawNoise(ctx, x, y, width, height) {
  const imageData = ctx.createImageData(width, height)
  const buffer32 = new Uint32Array(imageData.data.buffer)

  const LE = isLittleEndian();
                  // 0xAABBRRGG : 0xRRGGBBAA;
  const black = LE ? 0x80000000 : 0x00000080;
  const blue  = LE ? 0xFFFF0000 : 0x0000FFFF;

  for (let i = 0, len = buffer32.length; i < len; i++) {
    buffer32[i] = Math.random() < 0.5

      ? black
      : blue
  }

  ctx.putImageData(imageData, x, y)
}
function isLittleEndian() {
  const uint8 = new Uint8Array(8);
  const uint32 = new Uint32Array(uint8.buffer);
  uint8[0] = 255;
  return uint32[0] === 0XFF;
}
<canvas id="canvas"></canvas>
...