Почему моя функция шума холста всегда отображается только красным цветом?

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

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


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

/**
 * 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)
}

Интересно, как вы ожидаете, что изменение значений в копии imageData повлияет на imageData

Tibrogargan 06.06.2019 05:45

Кроме того ... imageData.data - это Uint8ClampedArray, а не UInt32Array ... принуждение к нему может не дать ожидаемых результатов. К тому же... imageData сама по себе является копией реального изображения.

Tibrogargan 06.06.2019 05:52

Я задал тот же вопрос. И все же это так.

Tom 06.06.2019 05:53
stackoverflow.com/questions/39422506/… .... или просто используйте Uint8ClampedArray и перестаньте пытаться принуждать данные
Tibrogargan 06.06.2019 05:56

Только что попробовал с Uint8ClampedArray. Работает. Очень странно, что изменение копии данных должно иметь какое-либо влияние на данные. Как ни странно ... использование UInt32Array тоже работает, но все время дает 100% непрозрачность. Красного нигде нет. Это не ваш код - это что-то другое. По крайней мере, не показанный код.

Tibrogargan 06.06.2019 06:27
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
5
252
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

[TLDR]:

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


Сначала давайте объясним «магия» за TypedArrays: массив буферов.

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

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

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

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

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

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

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

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

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

Сам Интерфейс 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.info(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>

Большое спасибо за объяснение всей магии ArrayBuffer. Все это имеет смысл. Я никогда не думал, что моя карьера веб-разработчика когда-либо будет заботиться о порядке следования байтов ... Сегодня вечером я возьму это на тест-драйв и отмечу ответ на основе этого.

Tom 06.06.2019 20:17

Разве 32-битный цвет с прямым порядком байтов не будет 0xBBAARRGG ? Обновлено: нет. Эксперименты на моей системе доказывают, что это 0xAaBbGgRr. Я подозреваю опечатку в вашем пояснительном комментарии к коду. В любом случае, я обнаружил еще одну брешь в своем плане, но вы полностью диагностировали проблему это. Большое вам спасибо за вашу помощь.

Tom 07.06.2019 03:26

@ Том действительно была опечатка, в моем примере 0x00112233 объяснение, вы заметили что-то еще?

Kaiido 07.06.2019 03:34

Оказывается, рисование прозрачного пикселя на холсте приводит к отображению основного документа, нет позволяет просвечивать существующие данные о цвете. Codepen, который я связал, создает шум, используя этот факт. Мне нужно будет прочитать данные изображения из области для шумоподавления, а затем использовать побитовую операцию (вероятно |), чтобы объединить значение шума с тем, что уже находится в этом месте холста. Я думаю, что я буду использовать Uint32Array для этого... :)

Tom 07.06.2019 03:39

yes putImageData заменяет фактический контент на InageData. Так что если вы поместите прозрачный пиксель там, где был непрозрачный, он будет прозрачным. Однако для того, что вы хотите сделать, рассмотрите возможность использования двух холстов и объедините шумовой один с видимым, используя составные операции, вы выиграете в производительности. Но на самом деле это может стоить отдельного вопроса/ответа ;-)

Kaiido 07.06.2019 03:43

Я на самом деле сделал всю эту штуку с двумя холстами, когда создавал свою собственную космическую игру с двойной буферизацией некоторое время назад! Я полагаю, я могу использовать getImageData вместо createImageData, а затем просто | каждый бит с моим значением шума/прозрачности. Затем я узнаю, нужно ли мне вообще возиться с putImageData — кажется, что прямой доступ к getImageData через ArrayBuffer может привести к немедленному изменению видимого холста.

Tom 07.06.2019 03:48

@ Том, а нет. Интерфейс ImageData не связан с буфером холста. В лучшем случае это «копия», когда вы делаете getImageData (на самом деле даже не потому, что пиксели не умножаются при передаче в ImageData). Так что придется ставить. Тогда выполнение компоновки графического процессора с использованием только drawImage и globalCompositeOperation будет быстрее, чем только getImageData.

Kaiido 07.06.2019 04:15

Другие вопросы по теме