javascript - 为什么我的 Canvas 噪点功能总是只显示红色?

标签 javascript canvas typed-arrays putimagedata

我正在尝试基于 codepen 将噪音效果应用到我的 Canvas 上我看到,这又似乎与 SO answer 非常相似.

我想生成一个由随机透明像素组成的“屏幕”,但我得到的是一个完全不透明的红色字段。我希望更熟悉 Canvas 或类型化数组的人可以告诉我我做错了什么,也许可以帮助我理解一些正在发挥作用的技术。


我对 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 位元素,反射(reflect)红、绿、蓝和32 位数组中每个像素的 alpha 值(串联),这允许我们将每个像素作为一个联合元组进行操作。我们得到一个每个像素一个元素的数组,而不是每个像素四个元素。

然后,我们迭代该数组中的元素,根据噪声逻辑将 RGBA 值写入每个元素(即每个像素)。这里的噪声逻辑非常简单:每个像素都有大约 50% 的机会成为“噪声”像素。

噪声像素被分配 32 位值 0x00000088,该值(得益于数组提供的 32 位分块)相当于 rgba(0, 0, 0, 0.5 ),即黑色,50% 不透明度。

非噪声像素被分配 32 位值 0x00000000,即黑色 0% 不透明度,即完全透明。

有趣的是,我们不会将 buffer32 写入 Canvas 。相反,我们编写了用于构造 Uint32ArrayimageData,这让我相信我们正在通过某种引用传递来改变 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

但它仍然是纯红色,纯黑色。底层 Canvas 数据不会通过本应不可见的像素显示。

令人困惑的是,除了红色或黑色之外,我无法获得任何颜色。除了 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%

发生什么事了?


编辑:基于 MDN article on ImageData.data 放弃 Uint32Array 和幽灵突变后的类似(坏)结果:

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

最佳答案

[TLDR]:

您的硬件的字节序设计为 LittleEndian,因此正确的十六进制格式是 0xAABBGGRR,而不是 0xRRGGBBAA


首先让我们解释一下 TypedArrays 背后的“魔力”:ArrayBuffers

ArrayBuffer 是一个非常特殊的对象,它直接链接到设备的内存。其本身 ArrayBuffer interface对我们来说没有太多功能,但是当您创建一个时,您实际上在内存中为您自己的脚本分配了它的长度。也就是说,js 引擎不会处理重新分配它、将其移动到其他地方、对其进行分块以及所有这些缓慢的操作,就像处理通常的 JS 对象一样。
因此,这使其成为操作二进制数据最快的对象之一。

但是,正如之前所说,它的界面本身非常有限。我们无法直接从 ArrayBuffer 访问数据,为此我们必须使用一个 view 对象,它不会复制数据,而只是提供一种直接访问数据的方法。

您可以对同一个 ArrayBuffer 有不同的 View ,但使用的数据始终只是 ArrayBuffer 中的一个,并且如果您从一个 View 编辑 ArrayBuffer,那么它将从另一个 View 可见:

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]

有不同类型的 View 对象,每种对象都提供不同的方式来表示分配给 ArrayBuffer 分配的内存槽的二进制数据。

TypedArrays像 Uint8Array、Float32Array 等都是 ArrayLike 接口(interface),它们提供了一种简单的方法来将数据作为数组进行操作,以自己的格式(8 位、Float32 等)表示数据。
DataView interface允许更开放的操作,例如甚至从通常无效的边界以不同格式读取,但是,这是以性能为代价的。

ImageData interface本身使用 ArrayBuffer 来存储其像素数据。默认情况下,它会公开此数据的 Uint8ClampedArray View 。也就是说,一个 ArrayLike 对象,每个 32 位像素按顺序表示为每个 channel 红色、绿色、蓝色和 Alpha 的从 0 到 255 的值。

因此,您的代码利用了 TypedArray 只是 View 对象这一事实,并且在底层 ArrayBuffer 上拥有其他 View 将直接修改它。
它的作者选择使用 Uint32Array,因为它是一种在单次拍摄中设置完整像素(记住 Canvas 图像是 32 位)的方法。您可以将所需的工作量减少四倍。

但是,这样做时,您将开始处理 32 位值。这可能会有点问题,因为现在 endianness很重要。
Uint8Array [0x00, 0x11, 0x22, 0x33] 在 BigEndian 系统中将表示为 32 位值 0x00112233,但在 LittleEndian 系统中表示为 0x33221100 .

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>

关于javascript - 为什么我的 Canvas 噪点功能总是只显示红色?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56470604/

相关文章:

javascript - 用于匹配 N 位数字加连续数字的正则表达式

javascript - Chart.js 设置 donut 背景颜色?

javascript - HTML5 和 Javascript Canvas 游戏 - 如何让玩家能够保存游戏

javascript - 访问 iframe 之外的元素?

javascript - 将可观察数组作为函数参数传递

javascript - Fabric JS 父子对象

javascript - 获取 Uint16Array 成员的单独低字节和高字节

javascript - 在 JavaScript 中将字节数组转换为二进制

javascript - Node 无法将原始二进制数据读入 Float32Array

javascript - HTML5 Canvas 工具提示