我正在尝试基于 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 。相反,我们编写了用于构造 Uint32Array
的 imageData
,这让我相信我们正在通过某种引用传递来改变 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/