我正在尝试根据我看到的代码笔将噪波效果应用于画布,这反过来似乎与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
的原始数据表示形式(一系列依次反映每个像素的红色,绿色,蓝色和alpha值的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%不透明外,我也无法获得任何透明性。为了说明断开连接,我删除了random元素,并尝试将这9个值中的每个值写入每个像素,以查看会发生什么:
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)
}
您的硬件的字节序设计为LittleEndian,因此正确的十六进制格式0xAABBGGRR
不是0xRRGGBBAA
。
首先让我们解释TypedArrays:ArrayBuffers背后的“魔力” 。
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.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等是它们提供一个简单的方法来操纵数据作为数组,表示在自己的格式的数据(8位,浮点32等)ArrayLike接口。
该数据视图界面允许更开放的操作一样,甚至无法正常无效边界以不同的格式读取,但是,它是以牺牲性能为代价。
所述接口的ImageData本身使用一个ArrayBuffer存储其像素数据。默认情况下,它将在此数据上公开一个Uint8ClampedArray视图。也就是说,一个ArrayLike对象,对于每个通道Red,Green,Blue和Alpha,每个32位像素均表示为从0到255的值。
因此,您的代码利用了TypedArrays只是视图对象这一事实,而在基础ArrayBuffer上具有其他视图将直接对其进行修改。
它的作者选择使用Uint32Array,因为它是一种在单个镜头中设置完整像素(记住画布图像为32位)的方法。您可以减少四倍的工作量。
但是,这样做会开始处理32位值。这可能会有点问题,因为现在字节顺序很重要。
Uint8Array在BigEndian系统[0x00, 0x11, 0x22, 0x33]
中将表示为32位值0x00112233
,但0x33221100
在LittleEndian系统中将表示为32bits值。
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>
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句